├── .gitignore ├── README.md ├── backend ├── config │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ ├── views │ │ ├── __init__.py │ │ └── auth.py │ └── wsgi.py ├── manage.py ├── notes │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20180705_0142.py │ │ ├── 0003_auto_20180705_0255.py │ │ ├── 0004_note_image.py │ │ ├── 0005_auto_20180711_0748.py │ │ ├── 0006_auto_20180711_2334.py │ │ ├── 0007_notestimeline.py │ │ ├── 0008_note_public.py │ │ ├── 0009_notestimeline_note.py │ │ ├── 0010_auto_20180719_0311.py │ │ ├── 0011_note_tags.py │ │ └── __init__.py │ ├── models.py │ ├── pagination.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── populate │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── requirements.txt └── tags │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── frontend ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── app │ ├── .htaccess │ ├── .nginx.conf │ ├── app.js │ ├── components │ │ ├── Header │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── LoginForm │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── NoteForm │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── NotesView │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── PaginatedTimelineView │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── PrivateNotesView │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── RegisterForm │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── SingleNote │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── TagsInput │ │ │ └── index.js │ │ ├── TagsSelector │ │ │ ├── Loadable.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── style.css │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── TagsWidget │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── TextInput │ │ │ └── index.js │ │ └── VoteForm │ │ │ ├── index.js │ │ │ └── tests │ │ │ └── index.test.js │ ├── configureStore.js │ ├── containers │ │ ├── App │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── index.test.js │ │ │ │ └── selectors.test.js │ │ ├── AuthProvider │ │ │ ├── index.js │ │ │ └── selectors.js │ │ ├── LanguageProvider │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ └── selectors.test.js │ │ ├── LoginPage │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ ├── saga.test.js │ │ │ │ └── selectors.test.js │ │ ├── LogoutPage │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── NotFoundPage │ │ │ ├── Loadable.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── NoteVoteWidget │ │ │ ├── Loadable.js │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ ├── saga.test.js │ │ │ │ └── selectors.test.js │ │ ├── NotesPage │ │ │ ├── Loadable.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ ├── saga.test.js │ │ │ │ └── selectors.test.js │ │ ├── PopularTags │ │ │ ├── Loadable.js │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ ├── saga.test.js │ │ │ │ └── selectors.test.js │ │ ├── RegisterPage │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ ├── saga.test.js │ │ │ │ └── selectors.test.js │ │ ├── TagTimeline │ │ │ ├── Loadable.js │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ ├── reducer.test.js │ │ │ │ ├── saga.test.js │ │ │ │ └── selectors.test.js │ │ └── Timeline │ │ │ ├── Loadable.js │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── messages.js │ │ │ ├── reducer.js │ │ │ ├── saga.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ ├── saga.test.js │ │ │ └── selectors.test.js │ ├── global-styles.js │ ├── i18n.js │ ├── images │ │ ├── favicon.ico │ │ └── icon-512x512.png │ ├── index.html │ ├── localStrorage.js │ ├── reducers.js │ ├── tests │ │ ├── i18n.test.js │ │ ├── reducers.test.js │ │ └── store.test.js │ ├── translations │ │ └── en.json │ └── utils │ │ ├── apiHandlers.js │ │ ├── checkStore.js │ │ ├── constants.js │ │ ├── injectReducer.js │ │ ├── injectSaga.js │ │ ├── middlewares.js │ │ ├── reducerInjectors.js │ │ ├── sagaInjectors.js │ │ └── tests │ │ ├── checkStore.test.js │ │ ├── injectReducer.test.js │ │ ├── injectSaga.test.js │ │ ├── reducerInjectors.test.js │ │ └── sagaInjectors.test.js ├── appveyor.yml ├── docs │ ├── README.md │ ├── css │ │ ├── README.md │ │ ├── linting.md │ │ ├── remove.md │ │ └── sanitize.md │ ├── general │ │ ├── README.md │ │ ├── commands.md │ │ ├── components.md │ │ ├── debugging.md │ │ ├── deployment.md │ │ ├── editor.md │ │ ├── faq.md │ │ ├── files.md │ │ ├── gotchas.md │ │ ├── introduction.md │ │ ├── remove.md │ │ ├── server-configs.md │ │ ├── webstorm-debug.png │ │ ├── webstorm-eslint.png │ │ └── workflow.png │ ├── js │ │ ├── README.md │ │ ├── async-components.md │ │ ├── i18n.md │ │ ├── immutablejs.md │ │ ├── redux-saga.md │ │ ├── redux.md │ │ ├── remove.md │ │ ├── reselect.md │ │ └── routing.md │ ├── maintenance │ │ └── dependency.md │ └── testing │ │ ├── README.md │ │ ├── component-testing.md │ │ ├── remote-testing.md │ │ └── unit-testing.md ├── internals │ ├── config.js │ ├── generators │ │ ├── component │ │ │ ├── class.js.hbs │ │ │ ├── index.js │ │ │ ├── loadable.js.hbs │ │ │ ├── messages.js.hbs │ │ │ ├── stateless.js.hbs │ │ │ └── test.js.hbs │ │ ├── container │ │ │ ├── actions.js.hbs │ │ │ ├── actions.test.js.hbs │ │ │ ├── class.js.hbs │ │ │ ├── constants.js.hbs │ │ │ ├── index.js │ │ │ ├── index.js.hbs │ │ │ ├── messages.js.hbs │ │ │ ├── reducer.js.hbs │ │ │ ├── reducer.test.js.hbs │ │ │ ├── saga.js.hbs │ │ │ ├── saga.test.js.hbs │ │ │ ├── selectors.js.hbs │ │ │ ├── selectors.test.js.hbs │ │ │ ├── stateless.js.hbs │ │ │ └── test.js.hbs │ │ ├── index.js │ │ ├── language │ │ │ ├── add-locale-data.hbs │ │ │ ├── app-locale.hbs │ │ │ ├── format-translation-messages.hbs │ │ │ ├── index.js │ │ │ ├── intl-locale-data.hbs │ │ │ ├── polyfill-intl-locale.hbs │ │ │ ├── translation-messages.hbs │ │ │ └── translations-json.hbs │ │ └── utils │ │ │ └── componentExists.js │ ├── mocks │ │ ├── cssModule.js │ │ └── image.js │ ├── scripts │ │ ├── analyze.js │ │ ├── clean.js │ │ ├── dependencies.js │ │ ├── extract-intl.js │ │ ├── generate-templates-for-linting.js │ │ ├── helpers │ │ │ ├── checkmark.js │ │ │ ├── progress.js │ │ │ └── xmark.js │ │ └── npmcheckversion.js │ ├── testing │ │ ├── enzyme-setup.js │ │ └── test-bundler.js │ └── webpack │ │ ├── webpack.base.babel.js │ │ ├── webpack.dev.babel.js │ │ ├── webpack.dll.babel.js │ │ └── webpack.prod.babel.js ├── package-lock.json ├── package.json ├── server │ ├── argv.js │ ├── index.js │ ├── logger.js │ ├── middlewares │ │ ├── addDevMiddlewares.js │ │ ├── addProdMiddlewares.js │ │ └── frontendMiddleware.js │ └── port.js └── yarn.lock └── notes.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | db.sqlite3 12 | env/ 13 | __pycache__/ 14 | uploads/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django & React :heart: 2 | An example of an app (Simple Notes) built using Django as a backend, and fully seperate React app as a front end. It uses jwt-token in user authorization. 3 | 4 | ## Screenshots 5 |

6 | 7 | 8 | 9 |

10 | 11 | ## How to run 12 | After cloning the repo, do the following to setup the app:
13 | 1- from the backend folder, execute those lines: 14 | ``` 15 | source env/bin/activate 16 | pip install -r requirements.txt 17 | python manage.py createsuperuser 18 | python manage.py runserver 19 | ``` 20 | 2- the django server should now be up and running.
21 | 3- from the frontend folder, execute those lines: 22 | ``` 23 | npm install 24 | npm start 25 | ``` 26 | 4- now go to the url located after `Access URLs:`
27 | 5- and you should see the website in the browser
28 | 29 | ## About the project 30 | In this project, I try to provide a simple boilerplate for anyone who wants to use Django as a backend, and offer jwt-auth based API.
31 | Also, I implemented a simple notes app example using React ([react-boilerplate](https://github.com/react-boilerplate/react-boilerplate)). In case you have any questions about the code, please feel free to ask.
32 | -------------------------------------------------------------------------------- /backend/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/config/__init__.py -------------------------------------------------------------------------------- /backend/config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from django.views import generic 4 | from rest_framework.schemas import get_schema_view 5 | from rest_framework_simplejwt.views import (TokenObtainPairView, TokenRefreshView) 6 | from .views.auth import * 7 | from django.conf import settings 8 | from django.conf.urls.static import static 9 | from django.core.exceptions import PermissionDenied 10 | from django.conf.urls.static import static 11 | 12 | from rest_framework.decorators import api_view 13 | from django.http import JsonResponse 14 | 15 | from populate.views import * 16 | 17 | @api_view(['GET']) 18 | def perform_test(request): 19 | ret = {'test': 2, 'fds': 3} 20 | return JsonResponse(ret) 21 | 22 | 23 | urlpatterns = [ 24 | path('admin/', admin.site.urls), 25 | path('', generic.RedirectView.as_view(url='/api/', permanent=False)), 26 | path('api/', get_schema_view(title="API Monitoring")), 27 | path('api/auth/', include('rest_framework.urls', namespace='rest_framework')), 28 | path('api/auth/token/obtain/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 29 | path('api/auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 30 | path('api/notes/', include('notes.urls')), 31 | path('api/testing/', perform_test), 32 | path('api/auth/register/', Registration.as_view()), 33 | path('api/tags/', include('tags.urls')), 34 | 35 | path('populate/users/', populate_users), 36 | path('populate/notes/', populate_notes), 37 | path('populate/note_votes/', populate_votes) 38 | 39 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 40 | -------------------------------------------------------------------------------- /backend/config/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseNotFound 2 | import datetime 3 | from rest_framework import views, serializers, status 4 | from rest_framework.response import Response 5 | from rest_framework.permissions import AllowAny 6 | 7 | 8 | def current_datetime(request): 9 | now = datetime.datetime.now() 10 | html = 'It is now %s.' % now 11 | 12 | return HttpResponseNotFound(html) 13 | 14 | 15 | class RegisterSeializer(serializers.Serializer): 16 | name = serializers.CharField(max_length=64) 17 | email = serializers.EmailField() 18 | password = serializers.CharField(max_length=16, min_length=6) 19 | 20 | class RegisterView(views.APIView): 21 | permission_classes = (AllowAny,) 22 | def post(self, request): 23 | serializer = RegisterSeializer(data=request.data) 24 | serializer.is_valid(raise_exception=True) 25 | return Response(serializer.data, status=status.HTTP_201_CREATED) 26 | -------------------------------------------------------------------------------- /backend/config/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/config/views/__init__.py -------------------------------------------------------------------------------- /backend/config/views/auth.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers, views 2 | from django.contrib.auth.models import User 3 | from django.http import HttpResponse, JsonResponse 4 | from django.views.decorators.csrf import csrf_exempt 5 | from rest_framework.renderers import JSONRenderer 6 | from rest_framework.parsers import JSONParser 7 | 8 | 9 | class RegistrationSerializer(serializers.ModelSerializer): 10 | class Meta: 11 | model = User 12 | fields = ('username', 'first_name', 'last_name', 'email', 'password') 13 | 14 | def create(self, validate_data): 15 | user = User( 16 | username=validate_data['username'], 17 | first_name=validate_data['first_name'], 18 | last_name=validate_data['last_name'], 19 | email=validate_data['email'] 20 | ) 21 | user.set_password(validate_data['password']) 22 | user.save() 23 | return user 24 | 25 | class Registration(views.APIView): 26 | 27 | def post(self, request): 28 | # data = JSONParser().parse(request) 29 | serializer = RegistrationSerializer(data=request.data) 30 | if serializer.is_valid(): 31 | serializer.save() 32 | print("user is saved!") 33 | return JsonResponse(serializer.data, status=201) 34 | 35 | print(serializer.errors) 36 | return JsonResponse(serializer.errors, status=401) 37 | -------------------------------------------------------------------------------- /backend/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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.0/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", "config.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /backend/notes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/notes/__init__.py -------------------------------------------------------------------------------- /backend/notes/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Note, NoteVote 3 | 4 | # Register your models here. 5 | admin.site.register(Note) 6 | admin.site.register(NoteVote) -------------------------------------------------------------------------------- /backend/notes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NotesConfig(AppConfig): 5 | name = 'notes' 6 | -------------------------------------------------------------------------------- /backend/notes/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-05 00:00 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Note', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(default='No Title', max_length=64)), 22 | ('body', models.TextField(default='')), 23 | ('created_at', models.DateField()), 24 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/notes/migrations/0002_auto_20180705_0142.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-05 01:42 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import notes.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('notes', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='note', 17 | name='updated_at', 18 | field=notes.models.AutoDateTimeField(default=django.utils.timezone.now), 19 | ), 20 | migrations.AlterField( 21 | model_name='note', 22 | name='created_at', 23 | field=models.DateField(default=django.utils.timezone.now, editable=False), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/notes/migrations/0003_auto_20180705_0255.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-05 02:55 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('notes', '0002_auto_20180705_0142'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='note', 16 | name='created_at', 17 | field=models.DateTimeField(default=django.utils.timezone.now, editable=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/notes/migrations/0004_note_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-11 06:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('notes', '0003_auto_20180705_0255'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='note', 15 | name='image', 16 | field=models.ImageField(blank=True, upload_to='notes/'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/notes/migrations/0005_auto_20180711_0748.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-11 07:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('notes', '0004_note_image'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='note', 15 | name='image', 16 | field=models.ImageField(blank=True, upload_to='uploads/'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/notes/migrations/0006_auto_20180711_2334.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-11 23:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('notes', '0005_auto_20180711_0748'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='note', 15 | name='image', 16 | field=models.ImageField(blank=True, null=True, upload_to='uploads/'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/notes/migrations/0007_notestimeline.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-14 02:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('notes', '0006_auto_20180711_2334'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='NotesTimeline', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ], 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/notes/migrations/0008_note_public.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-14 02:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('notes', '0007_notestimeline'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='note', 15 | name='public', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/notes/migrations/0009_notestimeline_note.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-14 09:00 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('notes', '0008_note_public'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='notestimeline', 16 | name='note', 17 | field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='notes.Note'), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/notes/migrations/0010_auto_20180719_0311.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-07-19 03:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('notes', '0009_notestimeline_note'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='NoteVote', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('like', models.BooleanField(default=True)), 21 | ('note', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='notes.Note')), 22 | ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 23 | ], 24 | ), 25 | migrations.RemoveField( 26 | model_name='notestimeline', 27 | name='note', 28 | ), 29 | migrations.DeleteModel( 30 | name='NotesTimeline', 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /backend/notes/migrations/0011_note_tags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-08-27 04:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('tags', '0001_initial'), 10 | ('notes', '0010_auto_20180719_0311'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='note', 16 | name='tags', 17 | field=models.ManyToManyField(to='tags.Tag'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/notes/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/notes/migrations/__init__.py -------------------------------------------------------------------------------- /backend/notes/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import CursorPagination 2 | 3 | class NotesTimelinePagination(CursorPagination): 4 | page_size = 10 5 | cursor_query_param = 'id' 6 | ordering = 'created_at' -------------------------------------------------------------------------------- /backend/notes/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/notes/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from .views import Notes, AllNotes, \ 3 | MakePublic, MakePrivate, NotesTimeline, upVote, downVote, PaginatedNotes 4 | 5 | 6 | urlpatterns = [ 7 | path('add/', Notes.as_view()), 8 | path('', AllNotes.as_view()), 9 | path('/makePublic', MakePublic), 10 | path('/makePrivate', MakePrivate), 11 | path('/upVote', upVote), 12 | path('/downVote', downVote), 13 | path('timeline/', NotesTimeline), 14 | path('timeline/paginated/', PaginatedNotes.as_view()), 15 | ] -------------------------------------------------------------------------------- /backend/populate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/populate/__init__.py -------------------------------------------------------------------------------- /backend/populate/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/populate/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PopulateConfig(AppConfig): 5 | name = 'populate' 6 | -------------------------------------------------------------------------------- /backend/populate/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/populate/migrations/__init__.py -------------------------------------------------------------------------------- /backend/populate/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/populate/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2018.4.16 2 | chardet==3.0.4 3 | coreapi==2.3.3 4 | coreschema==0.0.4 5 | Django==2.0.6 6 | djangorestframework==3.8.2 7 | djangorestframework-simplejwt==3.2.3 8 | idna==2.7 9 | itypes==1.1.0 10 | Jinja2==2.10 11 | MarkupSafe==1.0 12 | PyJWT==1.6.4 13 | pytz==2018.5 14 | requests==2.19.1 15 | uritemplate==3.0.0 16 | urllib3==1.23 17 | -------------------------------------------------------------------------------- /backend/tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/tags/__init__.py -------------------------------------------------------------------------------- /backend/tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Tag 4 | # Register your models here. 5 | admin.site.register(Tag) -------------------------------------------------------------------------------- /backend/tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TagsConfig(AppConfig): 5 | name = 'tags' 6 | -------------------------------------------------------------------------------- /backend/tags/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-08-27 04:13 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('notes', '0010_auto_20180719_0311'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Tag', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=16, unique=True)), 21 | ('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)), 22 | ('notes', models.ManyToManyField(to='notes.Note')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/tags/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/backend/tags/migrations/__init__.py -------------------------------------------------------------------------------- /backend/tags/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from django.utils import timezone 4 | from django.db.models import Avg, Count 5 | # from notes.models import Note 6 | 7 | class TagManager(models.Manager): 8 | def popular(self, count): 9 | return super().get_queryset().annotate(num_notes=Count('note')).order_by('-num_notes')[:count] 10 | 11 | 12 | class Tag(models.Model): 13 | title = models.CharField(max_length=16, unique=True) 14 | # notes = models.ManyToManyField(Note, blank=True) 15 | created_at = models.DateTimeField(default=timezone.now, editable=False) 16 | 17 | objects = TagManager() 18 | 19 | def __str__(self): 20 | return self.title 21 | 22 | def __unicode__(self): 23 | return self.title 24 | -------------------------------------------------------------------------------- /backend/tags/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/tags/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | from .views import AllTags, Notes, PopularTags 4 | 5 | urlpatterns = [ 6 | path('all/', AllTags.as_view()), 7 | path('popular/', PopularTags.as_view()), 8 | path('/', Notes.as_view()), 9 | ] -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const prettierOptions = JSON.parse(fs.readFileSync('./.prettierrc', 'utf8')); 4 | 5 | module.exports = { 6 | parser: 'babel-eslint', 7 | extends: ['airbnb', 'prettier', 'prettier/react'], 8 | plugins: ['prettier', 'redux-saga', 'react', 'jsx-a11y'], 9 | env: { 10 | browser: true, 11 | node: true, 12 | jest: true, 13 | es6: true, 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 6, 17 | sourceType: 'module', 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | }, 22 | rules: { 23 | 'prettier/prettier': ['error', prettierOptions], 24 | 'arrow-body-style': [2, 'as-needed'], 25 | 'class-methods-use-this': 0, 26 | 'comma-dangle': [2, 'always-multiline'], 27 | 'import/imports-first': 0, 28 | 'import/newline-after-import': 0, 29 | 'import/no-dynamic-require': 0, 30 | 'import/no-extraneous-dependencies': 0, 31 | 'import/no-named-as-default': 0, 32 | 'import/no-unresolved': 2, 33 | 'import/no-webpack-loader-syntax': 0, 34 | 'import/prefer-default-export': 0, 35 | indent: [ 36 | 2, 37 | 2, 38 | { 39 | SwitchCase: 1, 40 | }, 41 | ], 42 | 'jsx-a11y/aria-props': 2, 43 | 'jsx-a11y/heading-has-content': 0, 44 | 'jsx-a11y/label-has-for': 2, 45 | 'jsx-a11y/mouse-events-have-key-events': 2, 46 | 'jsx-a11y/role-has-required-aria-props': 2, 47 | 'jsx-a11y/role-supports-aria-props': 2, 48 | 'max-len': 0, 49 | 'newline-per-chained-call': 0, 50 | 'no-confusing-arrow': 0, 51 | 'no-console': 1, 52 | 'no-use-before-define': 0, 53 | 'prefer-template': 2, 54 | 'react/jsx-closing-tag-location': 0, 55 | 'react/forbid-prop-types': 0, 56 | 'react/jsx-first-prop-new-line': [2, 'multiline'], 57 | 'react/jsx-filename-extension': 0, 58 | 'react/jsx-no-target-blank': 0, 59 | 'react/require-default-props': 0, 60 | 'react/require-extension': 0, 61 | 'react/self-closing-comp': 0, 62 | 'react/sort-comp': 0, 63 | 'redux-saga/no-yield-in-race': 2, 64 | 'redux-saga/yield-effects': 2, 65 | 'require-yield': 0, 66 | }, 67 | settings: { 68 | 'import/resolver': { 69 | webpack: { 70 | config: './internals/webpack/webpack.prod.babel.js', 71 | }, 72 | }, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | .nginx.conf text 53 | 54 | # git config 55 | .gitattributes text 56 | .gitignore text 57 | .gitconfig text 58 | 59 | # code analysis config 60 | .jshintrc text 61 | .jscsrc text 62 | .jshintignore text 63 | .csslintrc text 64 | 65 | # misc config 66 | *.yaml text 67 | *.yml text 68 | .editorconfig text 69 | 70 | # build config 71 | *.npmignore text 72 | *.bowerrc text 73 | 74 | # Heroku 75 | Procfile text 76 | .slugignore text 77 | 78 | # Documentation 79 | *.md text 80 | LICENSE text 81 | AUTHORS text 82 | 83 | 84 | # 85 | ## These files are binary and should be left untouched 86 | # 87 | 88 | # (binary is a macro for -text -diff) 89 | *.png binary 90 | *.jpg binary 91 | *.jpeg binary 92 | *.gif binary 93 | *.ico binary 94 | *.mov binary 95 | *.mp4 binary 96 | *.mp3 binary 97 | *.flv binary 98 | *.fla binary 99 | *.swf binary 100 | *.gz binary 101 | *.zip binary 102 | *.7z binary 103 | *.ttf binary 104 | *.eot binary 105 | *.woff binary 106 | *.pyc binary 107 | *.pdf binary 108 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | lts/carbon 2 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | internals/generators/ 4 | internals/scripts/ 5 | package-lock.json 6 | yarn.lock 7 | package.json 8 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-styled-components"], 3 | "extends": [ 4 | "stylelint-config-recommended", 5 | "stylelint-config-styled-components" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 9 5 | - 8 6 | 7 | script: 8 | - node ./internals/scripts/generate-templates-for-linting 9 | - npm test -- --maxWorkers=4 10 | - npm run build 11 | 12 | before_install: 13 | - export CHROME_BIN=chromium-browser 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | 17 | notifications: 18 | email: 19 | on_failure: change 20 | 21 | after_success: 'npm run coveralls' 22 | 23 | cache: 24 | yarn: true 25 | directories: 26 | - node_modules 27 | -------------------------------------------------------------------------------- /frontend/app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | 46 | 47 | # Do not cache sw.js, required for offline-first updates. 48 | 49 | Header set Cache-Control "private, no-cache, no-store, proxy-revalidate, no-transform" 50 | Header set Pragma "no-cache" 51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/app/components/Header/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Header 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | // import styled from 'styled-components'; 10 | import { Switch, Route, Link } from 'react-router-dom'; 11 | import { FormattedMessage } from 'react-intl'; 12 | import messages from './messages'; 13 | import {PrivateRoute, AuthRoute, ShowIfAuth, ShowIfNotAuth} from 'containers/AuthProvider'; 14 | 15 | 16 | 17 | /* eslint-disable react/prefer-stateless-function */ 18 | class Header extends React.Component { 19 | render() { 20 | return ( 21 |
22 |

Public Timeline

23 |

Go to My Notes

} /> 24 |

Logout

} /> 25 |

Go to login

} /> 26 |
27 | ); 28 | } 29 | } 30 | 31 | Header.propTypes = {}; 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /frontend/app/components/Header/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Header Messages 3 | * 4 | * This contains all the text for the Header component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.Header.header', 12 | defaultMessage: 'This is the Header component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/Header/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import Header from '../index'; 5 | 6 | describe('
', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LoginForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | import TextInput from 'components/TextInput'; 10 | import {Alert, Button, Form} from 'reactstrap'; 11 | 12 | // import PropTypes from 'prop-types'; 13 | // import styled from 'styled-components'; 14 | 15 | 16 | /* eslint-disable react/prefer-stateless-function */ 17 | class LoginForm extends React.Component { 18 | 19 | state = { 20 | username: '', 21 | password: '', 22 | }; 23 | 24 | handleInputChange = (event) => { 25 | const target = event.target, 26 | value = target.type === 27 | 'checkbox' ? target.checked : target.value, 28 | name = target.name 29 | this.setState({ 30 | [name]: value 31 | }); 32 | } 33 | onSubmit = (e) => { 34 | if (e !== undefined && e.preventDefault) e.preventDefault(); 35 | this.props.onSubmit(this.state.username, this.state.password); 36 | } 37 | 38 | render() { 39 | const errors = this.props.errors || {} 40 | 41 | return ( 42 |
43 |
44 | { 45 | errors.non_field_errors ? 46 | 47 | {errors.non_field_errors} 48 | : "" 49 | } 50 | 52 | 54 | 55 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | LoginForm.propTypes = {}; 62 | 63 | export default LoginForm; 64 | -------------------------------------------------------------------------------- /frontend/app/components/LoginForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LoginForm Messages 3 | * 4 | * This contains all the text for the LoginForm component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.LoginForm.header', 12 | defaultMessage: 'This is the LoginForm component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/LoginForm/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import LoginForm from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/NoteForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NoteForm Messages 3 | * 4 | * This contains all the text for the NoteForm component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.NoteForm.header', 12 | defaultMessage: 'This is the NoteForm component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/NoteForm/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import NoteForm from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/NotesView/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * NotesView 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Row, Col } from 'reactstrap'; 9 | 10 | // import PropTypes from 'prop-types'; 11 | // import styled from 'styled-components'; 12 | import SingleNote from 'components/SingleNote'; 13 | import VoteForm from 'components/VoteForm'; 14 | import { FormattedMessage } from 'react-intl'; 15 | import messages from './messages'; 16 | 17 | /* eslint-disable react/prefer-stateless-function */ 18 | 19 | const ByUser = (props) => 20 | 23 | By: {props.name} 24 | ; 25 | 26 | class NotesView extends React.PureComponent { 27 | 28 | render() { 29 | const notes = this.props.notes.map( 30 | note => 31 | } /> 36 | ) 37 | notes.reverse() 38 | return ( 39 |
40 | {notes} 41 |
42 | ); 43 | } 44 | } 45 | 46 | NotesView.propTypes = {}; 47 | 48 | export default NotesView; 49 | -------------------------------------------------------------------------------- /frontend/app/components/NotesView/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotesView Messages 3 | * 4 | * This contains all the text for the NotesView component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.NotesView.header', 12 | defaultMessage: 'This is the NotesView component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/NotesView/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import NotesView from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/PaginatedTimelineView/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * NotesView 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Row, Col } from 'reactstrap'; 9 | 10 | // import PropTypes from 'prop-types'; 11 | // import styled from 'styled-components'; 12 | import SingleNote from 'components/SingleNote'; 13 | import VoteForm from 'components/VoteForm'; 14 | import { FormattedMessage } from 'react-intl'; 15 | import messages from './messages'; 16 | 17 | /* eslint-disable react/prefer-stateless-function */ 18 | 19 | const ByUser = (props) => 20 | 23 | By: {props.name} 24 | ; 25 | 26 | class PaginatedTimelineView extends React.Component { 27 | 28 | constructor(props) { 29 | super(props) 30 | this.fetchPaginatedNotesHandler = props.fetchPaginatedNotesHandler.bind(this) 31 | this.handleOnScroll = this.handleOnScroll.bind(this) 32 | } 33 | 34 | componentDidMount(){ 35 | window.addEventListener('scroll', this.handleOnScroll); 36 | } 37 | 38 | componentWillUnmount(){ 39 | window.removeEventListener('scroll', this.handleOnScroll); 40 | } 41 | 42 | handleOnScroll(){ 43 | let scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; 44 | let scrollHeight = (document.documentElement && document.documentElement.scrollHeight) || document.body.scrollHeight; 45 | let clientHeight = document.documentElement.clientHeight || window.innerHeight; 46 | let scrolledToBottom = Math.ceil(scrollTop + clientHeight) >= scrollHeight; 47 | 48 | if (scrolledToBottom) { 49 | this.fetchPaginatedNotesHandler(this.props.timeline.next_link); 50 | } 51 | } 52 | 53 | render() { 54 | const notes = this.props.timeline.notes.map( 55 | note => 56 | 60 | ) 61 | // notes.reverse() 62 | return ( 63 |
64 | {notes} 65 |
66 | ); 67 | } 68 | } 69 | 70 | PaginatedTimelineView.propTypes = {}; 71 | 72 | export default PaginatedTimelineView; 73 | -------------------------------------------------------------------------------- /frontend/app/components/PaginatedTimelineView/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotesView Messages 3 | * 4 | * This contains all the text for the NotesView component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.NotesView.header', 12 | defaultMessage: 'This is the NotesView component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/PaginatedTimelineView/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import NotesView from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/PrivateNotesView/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * NotesView 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | // import styled from 'styled-components'; 10 | import SingleNote from 'components/SingleNote'; 11 | import { FormGroup, Label, Input } from 'reactstrap'; 12 | 13 | const HandleNoteStatus = (props) => 14 | (
15 | 16 | 20 | 21 |
) 22 | 23 | 24 | /* eslint-disable react/prefer-stateless-function */ 25 | class NotesView extends React.PureComponent { 26 | 27 | render() { 28 | const notes = this.props.notes.map( 29 | note => { 30 | const handler = note.public? this.props.onNoteMakePrivate(note.id) : this.props.onNoteMakePublic(note.id); 31 | return (}/>) 34 | } 35 | ) 36 | notes.reverse() 37 | return ( 38 |
39 | {notes} 40 |
41 | ); 42 | } 43 | } 44 | 45 | NotesView.propTypes = {}; 46 | 47 | export default NotesView; 48 | -------------------------------------------------------------------------------- /frontend/app/components/PrivateNotesView/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotesView Messages 3 | * 4 | * This contains all the text for the NotesView component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.NotesView.header', 12 | defaultMessage: 'This is the NotesView component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/PrivateNotesView/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import NotesView from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/RegisterForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LoginForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | import TextInput from 'components/TextInput'; 10 | import {Alert, Button, Form} from 'reactstrap'; 11 | 12 | // import PropTypes from 'prop-types'; 13 | // import styled from 'styled-components'; 14 | 15 | 16 | /* eslint-disable react/prefer-stateless-function */ 17 | class RegisterForm extends React.Component { 18 | 19 | state = { 20 | username: '', 21 | first_name: '', 22 | last_name: '', 23 | email: '', 24 | password: '', 25 | }; 26 | 27 | handleInputChange = (event) => { 28 | const target = event.target, 29 | value = target.type === 30 | 'checkbox' ? target.checked : target.value, 31 | name = target.name 32 | this.setState({ 33 | [name]: value 34 | }); 35 | } 36 | 37 | onSubmit = (e) => { 38 | if (e !== undefined && e.preventDefault) e.preventDefault(); 39 | this.props.onSubmit(this.state) 40 | } 41 | 42 | render() { 43 | const errors = this.props.errors || {} 44 | 45 | return ( 46 |
47 |
48 | { 49 | errors.non_field_errors ? 50 | 51 | {errors.non_field_errors} 52 | : "" 53 | } 54 | 57 | 60 | 63 | 66 | 69 | 72 | 73 |
74 | ); 75 | } 76 | } 77 | 78 | RegisterForm.propTypes = {}; 79 | 80 | export default RegisterForm; 81 | -------------------------------------------------------------------------------- /frontend/app/components/RegisterForm/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LoginForm Messages 3 | * 4 | * This contains all the text for the LoginForm component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.LoginForm.header', 12 | defaultMessage: 'This is the LoginForm component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/RegisterForm/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import LoginForm from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/SingleNote/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SingleNote Messages 3 | * 4 | * This contains all the text for the SingleNote component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.SingleNote.header', 12 | defaultMessage: 'This is the SingleNote component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/SingleNote/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import SingleNote from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/TagsInput/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; 3 | 4 | import TagsSelector from 'components/TagsSelector'; 5 | 6 | 7 | export default ({ tags, handleSelectedTags, error, ...rest }) => { 8 | return ( 9 | 10 | 11 | 12 | {error ? ( 13 | {error} 14 | ) : ( 15 | '' 16 | )} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/app/components/TagsSelector/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for TagsSelector 4 | * 5 | */ 6 | 7 | import Loadable from 'react-loadable'; 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/components/TagsSelector/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TagsSelector 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | // import styled from 'styled-components'; 10 | 11 | import { FormattedMessage } from 'react-intl'; 12 | import messages from './messages'; 13 | 14 | 15 | import './style.css'; 16 | import { WithContext as ReactTags } from 'react-tag-input'; 17 | import {fetchTagsHandler} from 'utils/apiHandlers'; 18 | 19 | const KeyCodes = { 20 | comma: 188, 21 | enter: 13, 22 | }; 23 | 24 | const delimiters = [KeyCodes.comma, KeyCodes.enter]; 25 | 26 | /* eslint-disable react/prefer-stateless-function */ 27 | class TagsSelector extends React.Component { 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | suggestions: [], 32 | }; 33 | this.handleDelete = this.handleDelete.bind(this); 34 | this.handleAddition = this.handleAddition.bind(this); 35 | 36 | 37 | fetchTagsHandler(this.handleFetchTags); 38 | } 39 | 40 | handleDelete(i) { 41 | // const { tags } = this.state; 42 | // this.setState({ 43 | // tags: tags.filter((tag, index) => index !== i), 44 | // }); 45 | 46 | this.props.handleSelectedTags(this.props.tags.filter((tag, index) => index !== i)) 47 | } 48 | 49 | handleAddition(tag) { 50 | // this.setState(state => ({ tags: [...state.tags, tag] })); 51 | if(this.props.tags.length >= 3) return false 52 | let found = false; 53 | this.state.suggestions.forEach((suggestion) => {if(suggestion.id === tag.id) found = true}); 54 | if(found){ 55 | const currentTags = this.props.tags; 56 | this.props.handleSelectedTags([...currentTags, tag]) 57 | } 58 | } 59 | 60 | handleFetchTags = (newTags) => { 61 | const tags = newTags.map((tag) => ({id: String(tag.id), text: tag.title})); 62 | this.setState({ suggestions: tags }) 63 | }; 64 | 65 | render() { 66 | const { suggestions } = this.state; 67 | return ( 68 |
69 | 77 |
78 | ); 79 | } 80 | } 81 | 82 | TagsSelector.propTypes = {}; 83 | 84 | export default TagsSelector; 85 | -------------------------------------------------------------------------------- /frontend/app/components/TagsSelector/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TagsSelector Messages 3 | * 4 | * This contains all the text for the TagsSelector component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.TagsSelector.header', 12 | defaultMessage: 'This is the TagsSelector component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/TagsSelector/style.css: -------------------------------------------------------------------------------- 1 | 2 | div.ReactTags__tags { 3 | position: relative; 4 | } 5 | 6 | /* Styles for the input */ 7 | div.ReactTags__tagInput { 8 | width: 200px; 9 | border-radius: 2px; 10 | display: inline-block; 11 | } 12 | div.ReactTags__tagInput input.ReactTags__tagInputField, 13 | div.ReactTags__tagInput input.ReactTags__tagInputField:focus { 14 | height: 31px; 15 | margin: 0; 16 | font-size: 12px; 17 | width: 100%; 18 | border: 1px solid #eee; 19 | padding: 0 4px; 20 | } 21 | 22 | /* Styles for selected tags */ 23 | div.ReactTags__selected span.ReactTags__tag { 24 | border: 1px solid #ddd; 25 | background: #eee; 26 | font-size: 12px; 27 | display: inline-block; 28 | padding: 5px; 29 | margin: 0 5px; 30 | cursor: move; 31 | border-radius: 2px; 32 | } 33 | div.ReactTags__selected a.ReactTags__remove { 34 | color: #aaa; 35 | margin-left: 5px; 36 | cursor: pointer; 37 | } 38 | 39 | /* Styles for suggestions */ 40 | div.ReactTags__suggestions { 41 | position: absolute; 42 | } 43 | div.ReactTags__suggestions ul { 44 | list-style-type: none; 45 | box-shadow: .05em .01em .5em rgba(0,0,0,.2); 46 | background: white; 47 | width: 200px; 48 | } 49 | div.ReactTags__suggestions li { 50 | border-bottom: 1px solid #ddd; 51 | padding: 5px 10px; 52 | margin: 0; 53 | } 54 | div.ReactTags__suggestions li mark { 55 | text-decoration: underline; 56 | background: none; 57 | font-weight: 600; 58 | } 59 | div.ReactTags__suggestions ul li.ReactTags__activeSuggestion { 60 | background: #b7cfe0; 61 | cursor: pointer; 62 | } 63 | -------------------------------------------------------------------------------- /frontend/app/components/TagsSelector/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import TagsSelector from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/TagsWidget/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * TagsWidget 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | // import styled from 'styled-components'; 10 | 11 | import { FormattedMessage } from 'react-intl'; 12 | import messages from './messages'; 13 | import {Link} from "react-router-dom"; 14 | 15 | /* eslint-disable react/prefer-stateless-function */ 16 | class TagsWidget extends React.PureComponent { 17 | render() { 18 | console.log(this.props) 19 | const tags = this.props.tags.map( 20 | tag => { 21 | return ({tag.title} (total: {tag.num_notes} notes),
) 22 | } 23 | ); 24 | return ( 25 |
26 | {tags} 27 |
28 | ); 29 | } 30 | } 31 | 32 | TagsWidget.propTypes = {}; 33 | 34 | export default TagsWidget; 35 | -------------------------------------------------------------------------------- /frontend/app/components/TagsWidget/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TagsWidget Messages 3 | * 4 | * This contains all the text for the TagsWidget component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.TagsWidget.header', 12 | defaultMessage: 'This is the TagsWidget component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/components/TagsWidget/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import TagsWidget from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/components/TextInput/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormFeedback, FormGroup, Input, Label } from 'reactstrap'; 3 | 4 | export default ({ name, label, error, type, ...rest }) => { 5 | const id = `id_${name}`, 6 | input_type = type || 'text'; 7 | return ( 8 | 9 | {label ? : ''} 10 | 17 | {error ? ( 18 | {error} 19 | ) : ( 20 | '' 21 | )} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/app/components/VoteForm/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * VoteForm 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Row, Col } from 'reactstrap'; 9 | // import PropTypes from 'prop-types'; 10 | // import styled from 'styled-components'; 11 | 12 | /* eslint-disable react/prefer-stateless-function */ 13 | class VoteForm extends React.Component { 14 | render() { 15 | return ( 16 | 17 | up: {this.props.upvotes} 18 | 19 | 20 | down: {this.props.downvotes} 21 | 22 | ); 23 | } 24 | } 25 | 26 | VoteForm.propTypes = {}; 27 | 28 | export default VoteForm; 29 | -------------------------------------------------------------------------------- /frontend/app/components/VoteForm/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import VoteForm from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/configureStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with dynamic reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | 10 | import createReducer from './reducers'; 11 | import {logger, checkAccessToken} from "utils/middlewares"; 12 | 13 | const sagaMiddleware = createSagaMiddleware(); 14 | 15 | import {loadAuthState, saveAuthState} from "./localStrorage"; 16 | 17 | export default function configureStore(initialState = {}, history) { 18 | // Create the store with two middlewares 19 | // 1. sagaMiddleware: Makes redux-sagas work 20 | // 2. routerMiddleware: Syncs the location/URL path to the state 21 | const middlewares = [checkAccessToken, sagaMiddleware, routerMiddleware(history), logger]; 22 | 23 | const enhancers = [applyMiddleware(...middlewares)]; 24 | 25 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 26 | /* eslint-disable no-underscore-dangle, indent */ 27 | const composeEnhancers = 28 | process.env.NODE_ENV !== 'production' && 29 | typeof window === 'object' && 30 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 31 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 32 | // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading 33 | // Prevent recomputing reducers for `replaceReducer` 34 | shouldHotReload: false, 35 | }) 36 | : compose; 37 | /* eslint-enable */ 38 | 39 | const persistedState = loadAuthState() 40 | 41 | const store = createStore( 42 | createReducer(), 43 | fromJS(persistedState), 44 | composeEnhancers(...enhancers), 45 | ); 46 | // TODO find a fix for the multi errors appear with task id bein replicate 47 | // store.subscribe(() => { 48 | // 49 | // }); 50 | // store.subscribe(() => console.log(store.getState())) 51 | 52 | // Extensions 53 | store.runSaga = sagaMiddleware.run; 54 | store.injectedReducers = {}; // Reducer registry 55 | store.injectedSagas = {}; // Saga registry 56 | 57 | // Make reducers hot reloadable, see http://mxs.is/googmo 58 | /* istanbul ignore next */ 59 | if (module.hot) { 60 | module.hot.accept('./reducers', () => { 61 | store.replaceReducer(createReducer(store.injectedReducers)); 62 | }); 63 | } 64 | 65 | return store; 66 | } 67 | -------------------------------------------------------------------------------- /frontend/app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | * Each action has a corresponding type, which the reducer knows and picks up on. 4 | * To avoid weird typos between the reducer and the actions, we save them as 5 | * constants here. We prefix them with 'yourproject/YourComponent' so we avoid 6 | * reducers accidentally picking up actions they shouldn't. 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; 10 | */ 11 | -------------------------------------------------------------------------------- /frontend/app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a necessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | import { Switch, Route, Link, StaticRouter } from 'react-router-dom'; 16 | import 'bootstrap/dist/css/bootstrap.min.css'; 17 | 18 | import Timeline from 'containers/Timeline/Loadable'; 19 | import TagTimeline from 'containers/TagTimeline/Loadable'; 20 | import NotFoundPage from 'containers/NotFoundPage/Loadable'; 21 | import LoginPage from 'containers/LoginPage'; 22 | import RegisterPage from 'containers/RegisterPage'; 23 | import LogoutPage from 'containers/LogoutPage'; 24 | import NotesPage from 'containers/NotesPage'; 25 | import PopularTags from 'containers/PopularTags'; 26 | import {PrivateRoute, AuthRoute, ShowIfAuth} from 'containers/AuthProvider'; 27 | import Header from 'components/Header'; 28 | 29 | export default function App() { 30 | return ( 31 |
32 |

Personal Notes App

33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | ( 41 | )} /> 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /frontend/app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectRoute = state => state.get('route'); 4 | 5 | const makeSelectLocation = () => 6 | createSelector(selectRoute, routeState => routeState.get('location').toJS()); 7 | 8 | export { makeSelectLocation }; 9 | -------------------------------------------------------------------------------- /frontend/app/containers/App/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { Route } from 'react-router-dom'; 4 | 5 | import App from '../index'; 6 | 7 | describe('', () => { 8 | it('should render some routes', () => { 9 | const renderedComponent = shallow(); 10 | expect(renderedComponent.find(Route).length).not.toBe(0); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/containers/App/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { makeSelectLocation } from 'containers/App/selectors'; 4 | 5 | describe('makeSelectLocation', () => { 6 | it('should select the location', () => { 7 | const route = fromJS({ 8 | location: { pathname: '/foo' }, 9 | }); 10 | const mockedState = fromJS({ 11 | route, 12 | }); 13 | expect(makeSelectLocation()(mockedState)).toEqual( 14 | route.get('location').toJS(), 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/app/containers/AuthProvider/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Redirect, Route} from 'react-router-dom'; 3 | import {connect} from 'react-redux'; 4 | import {makeSelectAuth, isAuthenticated} from './selectors'; 5 | import {createStructuredSelector} from "reselect"; 6 | 7 | 8 | const PrivateRouteComponent = ({component: Component, isAuthenticated, ...rest}) => ( 9 | ( 10 | isAuthenticated ? ( 11 | 12 | ) : ( 13 | 17 | ) 18 | )}/> 19 | ) 20 | 21 | 22 | const mapStateToProps = createStructuredSelector({ 23 | isAuthenticated: isAuthenticated(), 24 | }); 25 | 26 | export const PrivateRoute = connect(mapStateToProps, null)(PrivateRouteComponent); 27 | 28 | const AuthRouteComponent = ({component: Component, isAuthenticated, ...rest}) => ( 29 | ( 30 | !isAuthenticated ? ( 31 | 32 | ) : ( 33 | 37 | ) 38 | )}/> 39 | ) 40 | 41 | export const AuthRoute = connect(mapStateToProps, null)(AuthRouteComponent); 42 | 43 | const ShowIfAuthComponent = ({component: Component, isAuthenticated, ...rest}) => ( 44 | isAuthenticated ? ( 45 | 46 | ) : ([]) 47 | ); 48 | 49 | export const ShowIfAuth = connect(mapStateToProps, null)(ShowIfAuthComponent); 50 | 51 | const ShowIfNotAuthComponent = ({component: Component, isAuthenticated, ...rest}) => ( 52 | !isAuthenticated ? ( 53 | 54 | ) : ([]) 55 | ); 56 | 57 | export const ShowIfNotAuth = connect(mapStateToProps, null)(ShowIfNotAuthComponent); 58 | -------------------------------------------------------------------------------- /frontend/app/containers/AuthProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import {createSelector} from 'reselect'; 2 | import {authInitialReducer} from '../../reducers'; 3 | 4 | 5 | export const selectAuthDomain = state => state.get('auth', authInitialReducer) 6 | 7 | /** 8 | * Other specific selectors 9 | */ 10 | 11 | 12 | export function isAccessTokenExpired(state) { 13 | if (state.access && state.access.exp) { 14 | return 1000 * state.access.exp - (new Date()).getTime() < 5000 15 | // return 1000 * state.access.exp - (new Date()).getTime() < 20000 16 | } 17 | return true 18 | } 19 | 20 | export function isRefreshTokenExpired(state) { 21 | if (state.refresh && state.refresh.exp) { 22 | return 1000 * state.refresh.exp - (new Date()).getTime() < 5000 23 | // return 1000 * state.refresh.exp - (new Date()).getTime() < 20000 24 | } 25 | return true 26 | } 27 | 28 | 29 | export const makeSelectAuth = () => 30 | createSelector(selectAuthDomain, substate => substate.toJS()); 31 | 32 | export const isAuthenticated = () => 33 | createSelector(selectAuthDomain, substate => !isRefreshTokenExpired(substate.toJS())); 34 | 35 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { CHANGE_LOCALE } from './constants'; 8 | 9 | export function changeLocale(languageLocale) { 10 | return { 11 | type: CHANGE_LOCALE, 12 | locale: languageLocale, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import PropTypes from 'prop-types'; 11 | import { connect } from 'react-redux'; 12 | import { createSelector } from 'reselect'; 13 | import { IntlProvider } from 'react-intl'; 14 | 15 | import { makeSelectLocale } from './selectors'; 16 | 17 | export class LanguageProvider extends React.PureComponent { 18 | // eslint-disable-line react/prefer-stateless-function 19 | render() { 20 | return ( 21 | 26 | {React.Children.only(this.props.children)} 27 | 28 | ); 29 | } 30 | } 31 | 32 | LanguageProvider.propTypes = { 33 | locale: PropTypes.string, 34 | messages: PropTypes.object, 35 | children: PropTypes.element.isRequired, 36 | }; 37 | 38 | const mapStateToProps = createSelector(makeSelectLocale(), locale => ({ 39 | locale, 40 | })); 41 | 42 | function mapDispatchToProps(dispatch) { 43 | return { 44 | dispatch, 45 | }; 46 | } 47 | 48 | export default connect( 49 | mapStateToProps, 50 | mapDispatchToProps, 51 | )(LanguageProvider); 52 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | 9 | import { CHANGE_LOCALE } from './constants'; 10 | import { DEFAULT_LOCALE } from '../../i18n'; // eslint-disable-line 11 | 12 | export const initialState = fromJS({ 13 | locale: DEFAULT_LOCALE, 14 | }); 15 | 16 | function languageProviderReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | return state.set('locale', action.locale); 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | export default languageProviderReducer; 26 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the languageToggle state domain 6 | */ 7 | const selectLanguage = state => state.get('language', initialState); 8 | 9 | /** 10 | * Select the language locale 11 | */ 12 | 13 | const makeSelectLocale = () => 14 | createSelector(selectLanguage, languageState => languageState.get('locale')); 15 | 16 | export { selectLanguage, makeSelectLocale }; 17 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { changeLocale } from '../actions'; 2 | 3 | import { CHANGE_LOCALE } from '../constants'; 4 | 5 | describe('LanguageProvider actions', () => { 6 | describe('Change Local Action', () => { 7 | it('has a type of CHANGE_LOCALE', () => { 8 | const expected = { 9 | type: CHANGE_LOCALE, 10 | locale: 'de', 11 | }; 12 | expect(changeLocale('de')).toEqual(expected); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import { FormattedMessage, defineMessages } from 'react-intl'; 4 | import { Provider } from 'react-redux'; 5 | import { browserHistory } from 'react-router-dom'; 6 | 7 | import ConnectedLanguageProvider, { LanguageProvider } from '../index'; 8 | import configureStore from '../../../configureStore'; 9 | 10 | import { translationMessages } from '../../../i18n'; 11 | 12 | const messages = defineMessages({ 13 | someMessage: { 14 | id: 'some.id', 15 | defaultMessage: 'This is some default message', 16 | en: 'This is some en message', 17 | }, 18 | }); 19 | 20 | describe('', () => { 21 | it('should render its children', () => { 22 | const children =

Test

; 23 | const renderedComponent = shallow( 24 | 25 | {children} 26 | , 27 | ); 28 | expect(renderedComponent.contains(children)).toBe(true); 29 | }); 30 | }); 31 | 32 | describe('', () => { 33 | let store; 34 | 35 | beforeAll(() => { 36 | store = configureStore({}, browserHistory); 37 | }); 38 | 39 | it('should render the default language messages', () => { 40 | const renderedComponent = mount( 41 | 42 | 43 | 44 | 45 | , 46 | ); 47 | expect( 48 | renderedComponent.contains( 49 | , 50 | ), 51 | ).toBe(true); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import languageProviderReducer from '../reducer'; 4 | import { CHANGE_LOCALE } from '../constants'; 5 | 6 | describe('languageProviderReducer', () => { 7 | it('returns the initial state', () => { 8 | expect(languageProviderReducer(undefined, {})).toEqual( 9 | fromJS({ 10 | locale: 'en', 11 | }), 12 | ); 13 | }); 14 | 15 | it('changes the locale', () => { 16 | expect( 17 | languageProviderReducer(undefined, { 18 | type: CHANGE_LOCALE, 19 | locale: 'de', 20 | }).toJS(), 21 | ).toEqual({ 22 | locale: 'de', 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/app/containers/LanguageProvider/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { selectLanguage } from '../selectors'; 4 | 5 | describe('selectLanguage', () => { 6 | it('should select the global state', () => { 7 | const globalState = fromJS({}); 8 | const mockedState = fromJS({ 9 | language: globalState, 10 | }); 11 | expect(selectLanguage(mockedState)).toEqual(globalState); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/LoginPage/DEFAULT_ACTION'; 8 | export const LOGIN_PROCESS = 'app/LoginPage/LOGIN_PROCESS'; 9 | export const LOGIN_SUCCESS = 'app/LoginPage/LOGIN_SUCCESS'; 10 | export const LOGIN_FAIL = 'app/LoginPage/LOGIN_FAIL'; 11 | 12 | export const LOGOUT_PROCESS = 'app/LoginPage/LOGOUT_PROCESS'; 13 | 14 | export const NEW_ACCESS_TOKEN = 'app/LoginPage/NEW_ACCESS_TOKEN'; 15 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LoginPage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import {connect} from 'react-redux'; 9 | import {Helmet} from 'react-helmet'; 10 | import {Link} from 'react-router-dom'; 11 | import {createStructuredSelector} from 'reselect'; 12 | import {compose} from 'redux'; 13 | 14 | import injectSaga from 'utils/injectSaga'; 15 | import injectReducer from 'utils/injectReducer'; 16 | import makeSelectLoginPage from './selectors'; 17 | import reducer from './reducer'; 18 | import saga from './saga'; 19 | import LoginForm from 'components/LoginForm'; 20 | import {LOGIN_PROCESS} from "./constants"; 21 | 22 | /* eslint-disable react/prefer-stateless-function */ 23 | export class LoginPage extends React.Component { 24 | render() { 25 | return ( 26 |
27 | 28 | LoginPage 29 | 30 | 31 |

Login page (Register)

32 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | // LoginPage.propTypes = { 39 | // dispatch: PropTypes.func.isRequired, 40 | // }; 41 | 42 | const mapStateToProps = createStructuredSelector({ 43 | loginpage: makeSelectLoginPage(), 44 | }); 45 | 46 | function mapDispatchToProps(dispatch) { 47 | return { 48 | onSubmitLogin: (username, password) => { 49 | console.log(username, password); 50 | // dispatch(handleLogin(username, password)); 51 | dispatch({ 52 | type: LOGIN_PROCESS, 53 | username: username, 54 | password: password, 55 | }); 56 | }, 57 | }; 58 | } 59 | 60 | const withConnect = connect( 61 | mapStateToProps, 62 | mapDispatchToProps, 63 | ); 64 | 65 | const withReducer = injectReducer({key: 'loginPage', reducer}); 66 | const withSaga = injectSaga({key: 'loginPage', saga}); 67 | 68 | export default compose( 69 | withReducer, 70 | withSaga, 71 | withConnect, 72 | )(LoginPage); 73 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LoginPage Messages 3 | * 4 | * This contains all the text for the LoginPage component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.LoginPage.header', 12 | defaultMessage: 'This is LoginPage container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | 9 | import { 10 | DEFAULT_ACTION, 11 | LOGIN_FAIL, 12 | LOGIN_PROCESS, 13 | LOGIN_SUCCESS, 14 | } from './constants'; 15 | import { loginHandler } from 'utils/apiHandlers'; 16 | 17 | export const initialState = fromJS({ 18 | errors: [], 19 | }); 20 | 21 | function loginPageReducer(state = initialState, action) { 22 | switch (action.type) { 23 | case DEFAULT_ACTION: 24 | return state; 25 | case LOGIN_PROCESS: 26 | return state.set('errors', []); 27 | case LOGIN_FAIL: 28 | return state.set('errors', action.errors); 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | export default loginPageReducer; 35 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/saga.js: -------------------------------------------------------------------------------- 1 | import { take, call, put, select, takeLatest} from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | import {LOGIN_FAIL, LOGIN_PROCESS, LOGIN_SUCCESS} from './constants'; 5 | import {loginHandler} from 'utils/apiHandlers'; 6 | 7 | 8 | export function* handleLogin(action) { 9 | 10 | console.log("in saga", action.username, action.password); 11 | try { 12 | const res = yield call(loginHandler, action.username, action.password); 13 | // console.log(res); 14 | // success login 15 | yield put({type: LOGIN_SUCCESS, auth: res}) 16 | // yield put(null) 17 | } catch (errs) { 18 | // yield 19 | // console.log(errs) 20 | yield put({type: LOGIN_FAIL, errors: errs}) 21 | } 22 | 23 | } 24 | export function* testFunc(action) { 25 | console.log("refresh token"); 26 | } 27 | export default function* defaultSaga() { 28 | // See example in containers/HomePage/saga.js 29 | yield takeLatest(LOGIN_PROCESS, handleLogin); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the loginPage state domain 6 | */ 7 | 8 | const selectLoginPageDomain = state => state.get('loginPage', initialState); 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | /** 15 | * Default selector used by LoginPage 16 | */ 17 | 18 | const makeSelectLoginPage = () => 19 | createSelector(selectLoginPageDomain, substate => substate.toJS()); 20 | 21 | export default makeSelectLoginPage; 22 | export { selectLoginPageDomain }; 23 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('LoginPage actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { LoginPage } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import loginPageReducer from '../reducer'; 3 | 4 | describe('loginPageReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(loginPageReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/LoginPage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectLoginPageDomain } from '../selectors'; 3 | 4 | describe('selectLoginPageDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/LogoutPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LogoutPage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | import { compose } from 'redux'; 11 | import {LOGOUT_PROCESS} from "../LoginPage/constants"; 12 | 13 | function LogoutPage(props) { 14 | props.dispatch({type: LOGOUT_PROCESS}); 15 | return
; 16 | } 17 | 18 | LogoutPage.propTypes = { 19 | dispatch: PropTypes.func.isRequired, 20 | }; 21 | 22 | function mapDispatchToProps(dispatch) { 23 | return { 24 | dispatch, 25 | }; 26 | } 27 | 28 | const withConnect = connect( 29 | null, 30 | mapDispatchToProps, 31 | ); 32 | 33 | export default compose(withConnect)(LogoutPage); 34 | -------------------------------------------------------------------------------- /frontend/app/containers/LogoutPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { LogoutPage } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/NotFoundPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously loads the component for NotFoundPage 3 | */ 4 | import Loadable from 'react-loadable'; 5 | 6 | export default Loadable({ 7 | loader: () => import('./index'), 8 | loading: () => null, 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a necessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | 15 | import messages from './messages'; 16 | 17 | /* eslint-disable react/prefer-stateless-function */ 18 | export default class NotFound extends React.PureComponent { 19 | render() { 20 | return ( 21 |

22 | 23 |

24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage component!', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/NotFoundPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import NotFoundPage from '../index'; 6 | import messages from '../messages'; 7 | 8 | describe('', () => { 9 | it('should render the page message', () => { 10 | const renderedComponent = shallow(); 11 | expect( 12 | renderedComponent.contains(), 13 | ).toEqual(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for NoteVoteWidget 4 | * 5 | */ 6 | 7 | import Loadable from 'react-loadable'; 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * NoteVoteWidget actions 4 | * 5 | */ 6 | 7 | import { DEFAULT_ACTION } from './constants'; 8 | 9 | export function defaultAction() { 10 | return { 11 | type: DEFAULT_ACTION, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * NoteVoteWidget constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/NoteVoteWidget/DEFAULT_ACTION'; 8 | export const UPVOTE_PROCESS = 'app/NotesPage/UPVOTE_PROCESS'; 9 | export const DOWNVOTE_PROCESS = 'app/NotesPage/DOWNVOTE_PROCESS'; 10 | export const UPVOTE_SUCCESS = 'app/NotesPage/UPVOTE_SUCCESS'; 11 | export const DOWNVOTE_SUCCESS = 'app/NotesPage/DOWNVOTE_SUCCESS'; 12 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * NoteVoteWidget 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | import { createStructuredSelector } from 'reselect'; 11 | import { compose } from 'redux'; 12 | 13 | import injectSaga from 'utils/injectSaga'; 14 | import injectReducer from 'utils/injectReducer'; 15 | import makeSelectNoteVoteWidget from './selectors'; 16 | import reducer from './reducer'; 17 | import saga from './saga'; 18 | import Col from "reactstrap/src/Col"; 19 | import Row from "reactstrap/src/Row"; 20 | import {UPVOTE_PROCESS, DOWNVOTE_PROCESS} from './constants'; 21 | 22 | /* eslint-disable react/prefer-stateless-function */ 23 | export class NoteVoteWidget extends React.Component { 24 | render() { 25 | return 26 | up: {this.props.upvotes} 27 | 28 | 29 | down: {this.props.downvotes} 30 | ; 31 | } 32 | } 33 | 34 | NoteVoteWidget.propTypes = { 35 | dispatch: PropTypes.func.isRequired, 36 | }; 37 | 38 | 39 | function mapDispatchToProps(dispatch) { 40 | return { 41 | onNoteUpVote: (note_id) => { 42 | dispatch({ 43 | type: UPVOTE_PROCESS, 44 | note_id: note_id, 45 | }); 46 | }, 47 | onNoteDownVote: (note_id) =>{ 48 | dispatch({ 49 | type: DOWNVOTE_PROCESS, 50 | note_id: note_id, 51 | }); 52 | }, 53 | }; 54 | } 55 | 56 | const withConnect = connect( 57 | null, 58 | mapDispatchToProps, 59 | ); 60 | 61 | const withReducer = injectReducer({ key: 'noteVoteWidget', reducer }); 62 | const withSaga = injectSaga({ key: 'noteVoteWidget', saga }); 63 | 64 | export default compose( 65 | withSaga, 66 | withConnect, 67 | )(NoteVoteWidget); 68 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * NoteVoteWidget reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { DEFAULT_ACTION } from './constants'; 9 | 10 | export const initialState = fromJS({}); 11 | 12 | function noteVoteWidgetReducer(state = initialState, action) { 13 | switch (action.type) { 14 | case DEFAULT_ACTION: 15 | return state; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default noteVoteWidgetReducer; 22 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/saga.js: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export default function* defaultSaga() { 5 | // See example in containers/HomePage/saga.js 6 | } 7 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the noteVoteWidget state domain 6 | */ 7 | 8 | const selectNoteVoteWidgetDomain = state => 9 | state.get('noteVoteWidget', initialState); 10 | 11 | /** 12 | * Other specific selectors 13 | */ 14 | 15 | /** 16 | * Default selector used by NoteVoteWidget 17 | */ 18 | 19 | const makeSelectNoteVoteWidget = () => 20 | createSelector(selectNoteVoteWidgetDomain, substate => substate.toJS()); 21 | 22 | export default makeSelectNoteVoteWidget; 23 | export { selectNoteVoteWidgetDomain }; 24 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('NoteVoteWidget actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { NoteVoteWidget } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import noteVoteWidgetReducer from '../reducer'; 3 | 4 | describe('noteVoteWidgetReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(noteVoteWidgetReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/NoteVoteWidget/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectNoteVoteWidgetDomain } from '../selectors'; 3 | 4 | describe('selectNoteVoteWidgetDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for NotesPage 4 | * 5 | */ 6 | 7 | import Loadable from 'react-loadable'; 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * NotesPage constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/NotesPage/DEFAULT_ACTION'; 8 | export const NOTE_ADD_PROCESS = 'app/NotesPage/NOTE_ADD_PROCESS'; 9 | export const NOTE_ADD_SUCCESS = 'app/NotesPage/NOTE_ADD_SUCCESS'; 10 | export const NOTE_ADD_FAIL = 'app/NotesPage/NOTE_ADD_FAIL'; 11 | export const NOTES_FETCH_PROCESS = 'app/NotesPage/NOTES_FETCH_PROCESS'; 12 | export const NOTES_FETCH_SUCCESS = 'app/NotesPage/NOTES_FETCH_SUCCESS'; 13 | 14 | export const NOTE_MAKEPUBLIC_PROCESS = 'app/NotesPage/NOTE_MAKEPUBLIC_PROCESS'; 15 | export const NOTE_MAKEPUBLIC_SUCCESS = 'app/NotesPage/NOTE_MAKEPUBLIC_SUCCESS'; 16 | export const NOTE_MAKEPRIVATE_PROCESS = 'app/NotesPage/NOTE_MAKEPRIVATE_PROCESS'; 17 | export const NOTE_MAKEPRIVATE_SUCCESS = 'app/NotesPage/NOTE_MAKEPRIVATE_SUCCESS'; 18 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotesPage Messages 3 | * 4 | * This contains all the text for the NotesPage component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.NotesPage.header', 12 | defaultMessage: 'This is NotesPage container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * NotesPage reducer 4 | * 5 | */ 6 | 7 | import {fromJS} from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | NOTE_ADD_FAIL, 11 | NOTE_ADD_PROCESS, 12 | NOTE_ADD_SUCCESS, 13 | NOTE_MAKEPUBLIC_SUCCESS, 14 | NOTE_MAKEPRIVATE_SUCCESS, 15 | NOTES_FETCH_SUCCESS 16 | } from './constants'; 17 | 18 | export const initialState = fromJS({ 19 | errors: [], 20 | notes: [], 21 | }); 22 | 23 | function notesPageReducer(state = initialState, action) { 24 | switch (action.type) { 25 | case DEFAULT_ACTION: 26 | return state; 27 | case NOTE_ADD_PROCESS: 28 | return state.set('errors', []); 29 | case NOTE_ADD_SUCCESS: 30 | return state.updateIn(['notes'], arr => arr.concat(action.note)); 31 | case NOTE_ADD_FAIL: 32 | return state.set('errors', action.errors); 33 | case NOTES_FETCH_SUCCESS: 34 | return state.set('notes', action.notes); 35 | case NOTE_MAKEPUBLIC_SUCCESS: 36 | var notes = state.get('notes').map(note => { 37 | if (note.id === action.note_id){ 38 | note.public = true; 39 | } 40 | return note 41 | }); 42 | return state.set('notes', notes); 43 | case NOTE_MAKEPRIVATE_SUCCESS: 44 | var notes = state.get('notes').map(note => { 45 | if (note.id === action.note_id){ 46 | note.public = false; 47 | } 48 | return note 49 | }); 50 | return state.set('notes', notes); 51 | default: 52 | return state; 53 | } 54 | } 55 | 56 | export default notesPageReducer; 57 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/saga.js: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | 4 | import {call, put, takeLatest} from "redux-saga/es/effects"; 5 | import {NOTE_ADD_PROCESS, NOTE_ADD_SUCCESS, NOTE_ADD_FAIL, 6 | NOTES_FETCH_PROCESS, NOTES_FETCH_SUCCESS, 7 | NOTE_MAKEPUBLIC_PROCESS, NOTE_MAKEPUBLIC_SUCCESS, 8 | NOTE_MAKEPRIVATE_PROCESS, NOTE_MAKEPRIVATE_SUCCESS} from "./constants"; 9 | import {handleLogin} from "../LoginPage/saga"; 10 | import {noteAddHandler, fetchNotesHandler, 11 | noteMakePublicHandler, noteMakePrivateHandler} from 'utils/apiHandlers'; 12 | 13 | export function* handleNoteAdd(action) { 14 | // console.log("in saga", action) 15 | try { 16 | const res = yield call(noteAddHandler, action.title, action.body, action.image, action.tags); 17 | // success login 18 | yield put({type: NOTE_ADD_SUCCESS, note: res}) 19 | // yield put(null) 20 | } catch (errs) { 21 | // yield 22 | console.log(errs) 23 | yield put({type: NOTE_ADD_FAIL, errors: errs}) 24 | } 25 | } 26 | 27 | export function* fetchNotes(action) { 28 | // console.log("in saga", action) 29 | try { 30 | const res = yield call(fetchNotesHandler); 31 | // success login 32 | yield put({type: NOTES_FETCH_SUCCESS, notes: res}) 33 | // yield put(null) 34 | } catch (errs) { 35 | // yield 36 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 37 | } 38 | } 39 | 40 | export function* handleMakePublic(action) { 41 | try { 42 | const res = yield call(noteMakePublicHandler, action.note_id); 43 | // success login 44 | yield put({type: NOTE_MAKEPUBLIC_SUCCESS, note_id: action.note_id}) 45 | // yield put(null) 46 | } catch (errs) { 47 | // yield 48 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 49 | } 50 | } 51 | export function* handleMakePrivate(action) { 52 | try { 53 | const res = yield call(noteMakePrivateHandler, action.note_id); 54 | // success login 55 | yield put({type: NOTE_MAKEPRIVATE_SUCCESS, note_id: action.note_id}) 56 | // yield put(null) 57 | } catch (errs) { 58 | // yield 59 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 60 | } 61 | } 62 | 63 | 64 | // Individual exports for testing 65 | export default function* defaultSaga() { 66 | // See example in containers/HomePage/saga.js 67 | yield takeLatest(NOTE_ADD_PROCESS, handleNoteAdd); 68 | yield takeLatest(NOTES_FETCH_PROCESS, fetchNotes); 69 | yield takeLatest(NOTE_MAKEPUBLIC_PROCESS, handleMakePublic) 70 | yield takeLatest(NOTE_MAKEPRIVATE_PROCESS, handleMakePrivate) 71 | } 72 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the notesPage state domain 6 | */ 7 | 8 | const selectNotesPageDomain = state => state.get('notesPage', initialState); 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | /** 15 | * Default selector used by NotesPage 16 | */ 17 | 18 | const makeSelectNotesPage = () => 19 | createSelector(selectNotesPageDomain, substate => substate.toJS()); 20 | 21 | export default makeSelectNotesPage; 22 | export { selectNotesPageDomain }; 23 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('NotesPage actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { NotesPage } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import notesPageReducer from '../reducer'; 3 | 4 | describe('notesPageReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(notesPageReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/NotesPage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectNotesPageDomain } from '../selectors'; 3 | 4 | describe('selectNotesPageDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for PopularTags 4 | * 5 | */ 6 | 7 | import Loadable from 'react-loadable'; 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PopularTags actions 4 | * 5 | */ 6 | 7 | import { DEFAULT_ACTION } from './constants'; 8 | 9 | export function defaultAction() { 10 | return { 11 | type: DEFAULT_ACTION, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PopularTags constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/PopularTags/DEFAULT_ACTION'; 8 | 9 | export const FETCH_TAGS_PROCESS = 'app/PopularTags/FETCH_TAGS_PROCESS'; 10 | export const FETCH_TAGS_SUCCESS = 'app/PopularTags/FETCH_TAGS_SUCCESS'; 11 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * PopularTags 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | import { FormattedMessage } from 'react-intl'; 11 | import { createStructuredSelector } from 'reselect'; 12 | import { compose } from 'redux'; 13 | 14 | import injectSaga from 'utils/injectSaga'; 15 | import injectReducer from 'utils/injectReducer'; 16 | import makeSelectPopularTags from './selectors'; 17 | import reducer from './reducer'; 18 | import saga from './saga'; 19 | import messages from './messages'; 20 | 21 | import {FETCH_TAGS_PROCESS, FETCH_TAGS_SUCCESS} from "./constants"; 22 | import TagsWidget from 'components/TagsWidget'; 23 | 24 | /* eslint-disable react/prefer-stateless-function */ 25 | export class PopularTags extends React.PureComponent { 26 | componentDidMount(){ 27 | this.props.fetchTags(); 28 | } 29 | 30 | render() { 31 | 32 | return ( 33 |
34 | {/**/} 35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | PopularTags.propTypes = { 42 | dispatch: PropTypes.func.isRequired, 43 | }; 44 | 45 | const mapStateToProps = createStructuredSelector({ 46 | populartags: makeSelectPopularTags(), 47 | }); 48 | 49 | function mapDispatchToProps(dispatch) { 50 | return { 51 | fetchTags: () => dispatch({ 52 | type: FETCH_TAGS_PROCESS, 53 | }), 54 | dispatch, 55 | }; 56 | } 57 | 58 | const withConnect = connect( 59 | mapStateToProps, 60 | mapDispatchToProps, 61 | ); 62 | 63 | const withReducer = injectReducer({ key: 'popularTags', reducer }); 64 | const withSaga = injectSaga({ key: 'popularTags', saga }); 65 | 66 | export default compose( 67 | withReducer, 68 | withSaga, 69 | withConnect, 70 | )(PopularTags); 71 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PopularTags Messages 3 | * 4 | * This contains all the text for the PopularTags component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.PopularTags.header', 12 | defaultMessage: 'This is PopularTags container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PopularTags reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { DEFAULT_ACTION } from './constants'; 9 | import {FETCH_TAGS_PROCESS, FETCH_TAGS_SUCCESS} from "./constants"; 10 | 11 | export const initialState = fromJS({ 12 | tags: [], 13 | }); 14 | 15 | function popularTagsReducer(state = initialState, action) { 16 | switch (action.type) { 17 | case DEFAULT_ACTION: 18 | return state; 19 | case FETCH_TAGS_SUCCESS: 20 | return state.set('tags', action.tags); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default popularTagsReducer; 27 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/saga.js: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | import {call, put, takeLatest} from "redux-saga/es/effects"; 3 | import {FETCH_TAGS_PROCESS, FETCH_TAGS_SUCCESS} from "./constants"; 4 | 5 | import {fetchPopularTagsHandler} from 'utils/apiHandlers'; 6 | 7 | export function* fetchTags(action) { 8 | // console.log("in saga", action) 9 | try { 10 | const res = yield call(fetchPopularTagsHandler); 11 | // success login 12 | yield put({type: FETCH_TAGS_SUCCESS, tags: res}) 13 | // yield put(null) 14 | } catch (errs) { 15 | // yield 16 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 17 | } 18 | } 19 | 20 | // Individual exports for testing 21 | export default function* defaultSaga() { 22 | // See example in containers/HomePage/saga.js 23 | yield takeLatest(FETCH_TAGS_PROCESS, fetchTags); 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the popularTags state domain 6 | */ 7 | 8 | const selectPopularTagsDomain = state => state.get('popularTags', initialState); 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | /** 15 | * Default selector used by PopularTags 16 | */ 17 | 18 | const makeSelectPopularTags = () => 19 | createSelector(selectPopularTagsDomain, substate => substate.toJS()); 20 | 21 | export default makeSelectPopularTags; 22 | export { selectPopularTagsDomain }; 23 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('PopularTags actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { PopularTags } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import popularTagsReducer from '../reducer'; 3 | 4 | describe('popularTagsReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(popularTagsReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/PopularTags/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectPopularTagsDomain } from '../selectors'; 3 | 4 | describe('selectPopularTagsDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/LoginPage/DEFAULT_ACTION'; 8 | export const REGISTER_PROCESS = 'app/LoginPage/REGISTER_PROCESS'; 9 | export const REGISTER_SUCCESS = 'app/LoginPage/REGISTER_SUCCESS'; 10 | export const REGISTER_FAIL = 'app/LoginPage/REGISTER_FAIL'; 11 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * RegisterPage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import {connect} from 'react-redux'; 9 | import {Helmet} from 'react-helmet'; 10 | import {Link, Redirect} from 'react-router-dom'; 11 | import {push} from 'react-router-redux'; 12 | import {createStructuredSelector} from 'reselect'; 13 | import {compose} from 'redux'; 14 | 15 | import injectSaga from 'utils/injectSaga'; 16 | import injectReducer from 'utils/injectReducer'; 17 | import makeSelectRegisterPage from './selectors'; 18 | import reducer from './reducer'; 19 | import saga from './saga'; 20 | import RegisterForm from 'components/RegisterForm'; 21 | import {REGISTER_PROCESS} from "./constants"; 22 | 23 | /* eslint-disable react/prefer-stateless-function */ 24 | export class RegisterPage extends React.Component { 25 | render() { 26 | // TODO need to find a better way for handling what happens when register is successful 27 | // const register_status = (this.props.registerpage.register_success)? 28 | // : []; 29 | return ( 30 |
31 | 32 | RegisterPage 33 | 34 | 35 |

Register page (Login)

36 | 37 |
38 | ); 39 | } 40 | } 41 | 42 | // RegisterPage.propTypes = { 43 | // dispatch: PropTypes.func.isRequired, 44 | // }; 45 | 46 | const mapStateToProps = createStructuredSelector({ 47 | registerpage: makeSelectRegisterPage(), 48 | }); 49 | 50 | function mapDispatchToProps(dispatch) { 51 | return { 52 | onSubmitRegister: (fields) => { 53 | dispatch({ 54 | type: REGISTER_PROCESS, 55 | fields: fields, 56 | callback: () => dispatch(push('/login')), 57 | }); 58 | 59 | }, 60 | }; 61 | } 62 | 63 | const withConnect = connect( 64 | mapStateToProps, 65 | mapDispatchToProps, 66 | ); 67 | 68 | const withReducer = injectReducer({key: 'registerPage', reducer}); 69 | const withSaga = injectSaga({key: 'registerPage', saga}); 70 | 71 | export default compose( 72 | withReducer, 73 | withSaga, 74 | withConnect, 75 | )(RegisterPage); 76 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LoginPage Messages 3 | * 4 | * This contains all the text for the LoginPage component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.LoginPage.header', 12 | defaultMessage: 'This is LoginPage container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage reducer 4 | * 5 | */ 6 | 7 | import {fromJS} from 'immutable'; 8 | import {Redirect, Route} from 'react-router-dom'; 9 | 10 | import {DEFAULT_ACTION, REGISTER_FAIL, REGISTER_PROCESS, REGISTER_SUCCESS} from './constants'; 11 | 12 | export const initialState = fromJS({ 13 | errors: [], 14 | }); 15 | 16 | function registerPageReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case DEFAULT_ACTION: 19 | return state; 20 | case REGISTER_PROCESS: 21 | return state.set('errors', []); 22 | case REGISTER_FAIL: 23 | return state.set('errors', action.errors); 24 | case REGISTER_SUCCESS: 25 | // return state.set('register_success', true) 26 | return state 27 | default: 28 | return state; 29 | } 30 | } 31 | 32 | export default registerPageReducer; 33 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/saga.js: -------------------------------------------------------------------------------- 1 | import { take, call, put, select, takeLatest} from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | import {DEFAULT_ACTION, REGISTER_FAIL, REGISTER_PROCESS, REGISTER_SUCCESS} from './constants'; 5 | import {registerHandler} from 'utils/apiHandlers'; 6 | 7 | 8 | export function* handleRegister(action) { 9 | // console.log(action) 10 | try { 11 | const res = yield call(registerHandler, action.fields); 12 | // console.log(res); 13 | // success login 14 | yield put({type: REGISTER_SUCCESS}) 15 | yield call(action.callback) 16 | // yield put(null) 17 | } catch (errs) { 18 | // yield 19 | // console.log(errs) 20 | yield put({type: REGISTER_FAIL, errors: errs}) 21 | } 22 | 23 | } 24 | 25 | export default function* defaultSaga() { 26 | // See example in containers/HomePage/saga.js 27 | yield takeLatest(REGISTER_PROCESS, handleRegister); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the loginPage state domain 6 | */ 7 | 8 | const selectRegisterPageDomain = state => state.get('registerPage', initialState); 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | /** 15 | * Default selector used by LoginPage 16 | */ 17 | 18 | const makeSelectRegisterPage = () => 19 | createSelector(selectRegisterPageDomain, substate => substate.toJS()); 20 | 21 | export default makeSelectRegisterPage; 22 | export { selectRegisterPageDomain }; 23 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('LoginPage actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { LoginPage } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import loginPageReducer from '../reducer'; 3 | 4 | describe('loginPageReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(loginPageReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/RegisterPage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectLoginPageDomain } from '../selectors'; 3 | 4 | describe('selectLoginPageDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Timeline 4 | * 5 | */ 6 | import Loadable from 'react-loadable'; 7 | 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Timeline actions 4 | * 5 | */ 6 | 7 | import { DEFAULT_ACTION } from './constants'; 8 | 9 | export function defaultAction() { 10 | return { 11 | type: DEFAULT_ACTION, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Timeline constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/Timeline/DEFAULT_ACTION'; 8 | export const TIMELINE_FETCH_PROCESS = 'app/NotesPage/TIMELINE_FETCH_PROCESS'; 9 | export const TIMELINE_FETCH_SUCCESS = 'app/NotesPage/TIMELINE_FETCH_SUCCESS'; 10 | export const PAGINATED_TIMELINE_FETCH_SUCCESS = 'app/NotesPage/PAGINATED_TIMELINE_FETCH_SUCCESS'; 11 | export const PAGINATED_TIMELINE_FETCH_PROCESS = 'app/NotesPage/PAGINATED_TIMELINE_FETCH_PROCESS'; 12 | export const TAG_TIMELINE_FETCH_PROCESS = 'app/NotesPage/TAG_TIMELINE_FETCH_PROCESS'; 13 | export const TAG_TIMELINE_FETCH_SUCCESS = 'app/NotesPage/TAG_TIMELINE_FETCH_SUCCESS'; 14 | export const UPVOTE_PROCESS = 'app/NotesPage/UPVOTE_PROCESS'; 15 | export const DOWNVOTE_PROCESS = 'app/NotesPage/DOWNVOTE_PROCESS'; 16 | export const UPVOTE_SUCCESS = 'app/NotesPage/UPVOTE_SUCCESS'; 17 | export const DOWNVOTE_SUCCESS = 'app/NotesPage/DOWNVOTE_SUCCESS'; 18 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Timeline Messages 3 | * 4 | * This contains all the text for the Timeline component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.Timeline.header', 12 | defaultMessage: 'This is Timeline container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Timeline reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import {DEFAULT_ACTION, UPVOTE_PROCESS, DOWNVOTE_PROCESS, 9 | UPVOTE_SUCCESS, DOWNVOTE_SUCCESS, 10 | TIMELINE_FETCH_PROCESS, TIMELINE_FETCH_SUCCESS, PAGINATED_TIMELINE_FETCH_SUCCESS, TAG_TIMELINE_FETCH_SUCCESS, TAG_TIMELINE_FETCH_PROCESS} from "./constants"; 11 | 12 | export const initialState = fromJS({ 13 | notes: [], 14 | tag_title: null, 15 | next_link: null, 16 | }); 17 | 18 | function timelineReducer(state = initialState, action) { 19 | switch (action.type) { 20 | case DEFAULT_ACTION: 21 | return state; 22 | case TIMELINE_FETCH_SUCCESS: 23 | return state.set('notes', action.notes); 24 | case TAG_TIMELINE_FETCH_PROCESS: 25 | if(action.isInitial === true) return initialState.set('tag_title', action.tag_title); 26 | return state.set('tag_title', action.tag_title); 27 | case TAG_TIMELINE_FETCH_SUCCESS: 28 | // if(action.isInitial){ 29 | // state = state.set('notes', action.notes); 30 | // }else{ 31 | // state = state.update('notes', list => list.push(...action.notes)); 32 | // } 33 | return state.update('notes', list => list.push(...action.notes)).set('next_link', action.next_link); 34 | case DOWNVOTE_SUCCESS: 35 | case UPVOTE_SUCCESS: 36 | let notes = state.get('notes').map(note => { 37 | if (note.id === action.note_id){ 38 | note.votes = action.new_votes; 39 | } 40 | return note 41 | }); 42 | return state.set('notes', notes); 43 | default: 44 | return state; 45 | } 46 | } 47 | 48 | export default timelineReducer; 49 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the timeline state domain 6 | */ 7 | 8 | const selectTagTimelineDomain = state => state.get('tag_timeline', initialState); 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | /** 15 | * Default selector used by Timeline 16 | */ 17 | 18 | const makeTagSelectTimeline = () => 19 | createSelector(selectTagTimelineDomain, substate => substate.toJS()); 20 | 21 | export default makeTagSelectTimeline; 22 | export { selectTagTimelineDomain }; 23 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('Timeline actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { Timeline } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import timelineReducer from '../reducer'; 3 | 4 | describe('timelineReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(timelineReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/TagTimeline/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectTimelineDomain } from '../selectors'; 3 | 4 | describe('selectTimelineDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Timeline 4 | * 5 | */ 6 | import Loadable from 'react-loadable'; 7 | 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Timeline actions 4 | * 5 | */ 6 | 7 | import { DEFAULT_ACTION } from './constants'; 8 | 9 | export function defaultAction() { 10 | return { 11 | type: DEFAULT_ACTION, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Timeline constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/Timeline/DEFAULT_ACTION'; 8 | export const TIMELINE_FETCH_PROCESS = 'app/NotesPage/TIMELINE_FETCH_PROCESS'; 9 | export const TIMELINE_FETCH_SUCCESS = 'app/NotesPage/TIMELINE_FETCH_SUCCESS'; 10 | export const PAGINATED_TIMELINE_FETCH_SUCCESS = 'app/NotesPage/PAGINATED_TIMELINE_FETCH_SUCCESS'; 11 | export const PAGINATED_TIMELINE_FETCH_PROCESS = 'app/NotesPage/PAGINATED_TIMELINE_FETCH_PROCESS'; 12 | export const UPVOTE_PROCESS = 'app/NotesPage/UPVOTE_PROCESS'; 13 | export const DOWNVOTE_PROCESS = 'app/NotesPage/DOWNVOTE_PROCESS'; 14 | export const UPVOTE_SUCCESS = 'app/NotesPage/UPVOTE_SUCCESS'; 15 | export const DOWNVOTE_SUCCESS = 'app/NotesPage/DOWNVOTE_SUCCESS'; 16 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Timeline Messages 3 | * 4 | * This contains all the text for the Timeline component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.Timeline.header', 12 | defaultMessage: 'This is Timeline container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Timeline reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import {DEFAULT_ACTION, UPVOTE_PROCESS, DOWNVOTE_PROCESS, 9 | UPVOTE_SUCCESS, DOWNVOTE_SUCCESS, 10 | TIMELINE_FETCH_PROCESS, TIMELINE_FETCH_SUCCESS, PAGINATED_TIMELINE_FETCH_SUCCESS, PAGINATED_TIMELINE_FETCH_PROCESS} from "./constants"; 11 | 12 | export const initialState = fromJS({ 13 | notes: [], 14 | next_link: null, 15 | }); 16 | 17 | function timelineReducer(state = initialState, action) { 18 | switch (action.type) { 19 | case DEFAULT_ACTION: 20 | return state; 21 | case TIMELINE_FETCH_SUCCESS: 22 | return state.set('notes', action.notes); 23 | case PAGINATED_TIMELINE_FETCH_PROCESS: 24 | if(action.isInitial === true) return initialState; 25 | return state; 26 | case PAGINATED_TIMELINE_FETCH_SUCCESS: 27 | return state.update('notes', list => list.push(...action.notes)).set('next_link', action.next_link); 28 | case DOWNVOTE_SUCCESS: 29 | case UPVOTE_SUCCESS: 30 | let notes = state.get('notes').map(note => { 31 | if (note.id === action.note_id){ 32 | note.votes = action.new_votes; 33 | } 34 | return note 35 | }); 36 | return state.set('notes', notes); 37 | default: 38 | return state; 39 | } 40 | } 41 | 42 | export default timelineReducer; 43 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/saga.js: -------------------------------------------------------------------------------- 1 | import {call, put, takeLatest} from "redux-saga/es/effects"; 2 | import {UPVOTE_PROCESS, DOWNVOTE_PROCESS, 3 | UPVOTE_SUCCESS, DOWNVOTE_SUCCESS, 4 | TIMELINE_FETCH_PROCESS, TIMELINE_FETCH_SUCCESS, PAGINATED_TIMELINE_FETCH_PROCESS, PAGINATED_TIMELINE_FETCH_SUCCESS} from "./constants"; 5 | import {fetchTimelineHandler, noteUpVoteHandler, noteDownVoteHandler, fetchPaginatedTimelineHandler} from 'utils/apiHandlers'; 6 | 7 | export function* fetchTimeline(action) { 8 | try { 9 | const res = yield call(fetchTimelineHandler); 10 | yield put({type: TIMELINE_FETCH_SUCCESS, notes: res}) 11 | } catch (errs) { 12 | // yield 13 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 14 | } 15 | } 16 | 17 | // for testing a paginated timeline 18 | export function* fetchPaginatedTimeline(action) { 19 | try{ 20 | const res = yield call(fetchPaginatedTimelineHandler, action.next_link); 21 | yield put({type: PAGINATED_TIMELINE_FETCH_SUCCESS, notes: res.results, next_link: res.next}) 22 | } catch (errs) { 23 | 24 | } 25 | } 26 | 27 | export function* handleUpVote(action) { 28 | try { 29 | const res = yield call(noteUpVoteHandler, action.note_id); 30 | // success login 31 | yield put({type: UPVOTE_SUCCESS, note_id: action.note_id, new_votes: res}) 32 | // yield put(null) 33 | } catch (errs) { 34 | // yield 35 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 36 | } 37 | } 38 | 39 | export function* handleDownVote(action) { 40 | try { 41 | const res = yield call(noteDownVoteHandler, action.note_id); 42 | // success login 43 | yield put({type: DOWNVOTE_SUCCESS, note_id: action.note_id, new_votes: res}) 44 | // yield put(null) 45 | } catch (errs) { 46 | // yield 47 | // yield put({type: NOTE_ADD_FAIL, errors: errs}) 48 | } 49 | } 50 | // Individual exports for testing 51 | export default function* defaultSaga() { 52 | // See example in containers/HomePage/saga.js 53 | yield takeLatest(TIMELINE_FETCH_PROCESS, fetchTimeline); 54 | yield takeLatest(PAGINATED_TIMELINE_FETCH_PROCESS, fetchPaginatedTimeline); 55 | yield takeLatest(UPVOTE_PROCESS, handleUpVote); 56 | yield takeLatest(DOWNVOTE_PROCESS, handleDownVote); 57 | } 58 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the timeline state domain 6 | */ 7 | 8 | const selectTimelineDomain = state => state.get('timeline', initialState); 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | /** 15 | * Default selector used by Timeline 16 | */ 17 | 18 | const makeSelectTimeline = () => 19 | createSelector(selectTimelineDomain, substate => substate.toJS()); 20 | 21 | export default makeSelectTimeline; 22 | export { selectTimelineDomain }; 23 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('Timeline actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { Timeline } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import timelineReducer from '../reducer'; 3 | 4 | describe('timelineReducer', () => { 5 | it('returns the initial state', () => { 6 | expect(timelineReducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/app/containers/Timeline/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectTimelineDomain } from '../selectors'; 3 | 4 | describe('selectTimelineDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | 3 | // eslint no-unused-expressions: 0 4 | injectGlobal` 5 | html, 6 | body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 13 | } 14 | 15 | body.fontLoaded { 16 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #app { 20 | background-color: #fafafa; 21 | min-height: 100%; 22 | min-width: 100%; 23 | } 24 | 25 | p, 26 | label { 27 | font-family: Georgia, Times, 'Times New Roman', serif; 28 | line-height: 1.5em; 29 | } 30 | .container{ 31 | width: 400px; 32 | margin: 0 auto; 33 | } 34 | `; 35 | 36 | -------------------------------------------------------------------------------- /frontend/app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | * IMPORTANT: This file is used by the internal build 7 | * script `extract-intl`, and must use CommonJS module syntax 8 | * You CANNOT use import/export in this file. 9 | */ 10 | const addLocaleData = require('react-intl').addLocaleData; //eslint-disable-line 11 | const enLocaleData = require('react-intl/locale-data/en'); 12 | 13 | const enTranslationMessages = require('./translations/en.json'); 14 | 15 | addLocaleData(enLocaleData); 16 | 17 | const DEFAULT_LOCALE = 'en'; 18 | 19 | // prettier-ignore 20 | const appLocales = [ 21 | 'en', 22 | ]; 23 | 24 | const formatTranslationMessages = (locale, messages) => { 25 | const defaultFormattedMessages = 26 | locale !== DEFAULT_LOCALE 27 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) 28 | : {}; 29 | const flattenFormattedMessages = (formattedMessages, key) => { 30 | const formattedMessage = 31 | !messages[key] && locale !== DEFAULT_LOCALE 32 | ? defaultFormattedMessages[key] 33 | : messages[key]; 34 | return Object.assign(formattedMessages, { [key]: formattedMessage }); 35 | }; 36 | return Object.keys(messages).reduce(flattenFormattedMessages, {}); 37 | }; 38 | 39 | const translationMessages = { 40 | en: formatTranslationMessages('en', enTranslationMessages), 41 | }; 42 | 43 | exports.appLocales = appLocales; 44 | exports.formatTranslationMessages = formatTranslationMessages; 45 | exports.translationMessages = translationMessages; 46 | exports.DEFAULT_LOCALE = DEFAULT_LOCALE; 47 | -------------------------------------------------------------------------------- /frontend/app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/frontend/app/images/favicon.ico -------------------------------------------------------------------------------- /frontend/app/images/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/frontend/app/images/icon-512x512.png -------------------------------------------------------------------------------- /frontend/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React.js Boilerplate 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/app/localStrorage.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const loadAuthState = () => { 4 | try{ 5 | const serializedState = localStorage.getItem('state'); 6 | if(serializedState === null) { 7 | return undefined; 8 | } 9 | let JSONparsed = JSON.parse(serializedState) 10 | if(JSONparsed.auth.access.token !== undefined){ 11 | axios.defaults.headers.common['Authorization'] = `Bearer ${JSONparsed.auth.access.token}`; 12 | } 13 | return JSONparsed; 14 | } catch (e) { 15 | return undefined; 16 | } 17 | } 18 | 19 | export const saveAuthState = (state) => { 20 | try { 21 | const serializedState = JSON.stringify(state); 22 | localStorage.setItem('state', serializedState) 23 | } catch (e) { 24 | // ignore 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/app/tests/i18n.test.js: -------------------------------------------------------------------------------- 1 | import { formatTranslationMessages } from '../i18n'; 2 | 3 | jest.mock('../translations/en.json', () => ({ 4 | message1: 'default message', 5 | message2: 'default message 2', 6 | })); 7 | 8 | const esTranslationMessages = { 9 | message1: 'mensaje predeterminado', 10 | message2: '', 11 | }; 12 | 13 | describe('formatTranslationMessages', () => { 14 | it('should build only defaults when DEFAULT_LOCALE', () => { 15 | const result = formatTranslationMessages('en', { a: 'a' }); 16 | 17 | expect(result).toEqual({ a: 'a' }); 18 | }); 19 | 20 | it('should combine default locale and current locale when not DEFAULT_LOCALE', () => { 21 | const result = formatTranslationMessages('', esTranslationMessages); 22 | 23 | expect(result).toEqual({ 24 | message1: 'mensaje predeterminado', 25 | message2: 'default message 2', 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /frontend/app/tests/reducers.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test route reducer 3 | */ 4 | 5 | import { fromJS } from 'immutable'; 6 | import { LOCATION_CHANGE } from 'react-router-redux'; 7 | import { routeReducer } from '../reducers'; 8 | 9 | describe('route reducer', () => { 10 | it('should return the initial state', () => { 11 | const initialState = { foo: 'bar' }; 12 | expect(routeReducer(initialState, {})).toEqual(initialState); 13 | }); 14 | 15 | it('should handle the location_change action correctly', () => { 16 | const state = fromJS({ location: 'somewhere' }); 17 | const payload = 'elsewhere'; 18 | const action = { type: LOCATION_CHANGE, payload }; 19 | 20 | const expectedState = { location: payload }; 21 | const resultState = routeReducer(state, action).toJS(); 22 | expect(resultState).toEqual(expectedState); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import { browserHistory } from 'react-router-dom'; 6 | import configureStore from '../configureStore'; 7 | 8 | describe('configureStore', () => { 9 | let store; 10 | 11 | beforeAll(() => { 12 | store = configureStore({}, browserHistory); 13 | }); 14 | 15 | describe('injectedReducers', () => { 16 | it('should contain an object for reducers', () => { 17 | expect(typeof store.injectedReducers).toBe('object'); 18 | }); 19 | }); 20 | 21 | describe('injectedSagas', () => { 22 | it('should contain an object for sagas', () => { 23 | expect(typeof store.injectedSagas).toBe('object'); 24 | }); 25 | }); 26 | 27 | describe('runSaga', () => { 28 | it('should contain a hook for `sagaMiddleware.run`', () => { 29 | expect(typeof store.runSaga).toBe('function'); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('configureStore params', () => { 35 | it('should call window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__', () => { 36 | /* eslint-disable no-underscore-dangle */ 37 | const compose = jest.fn(); 38 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = () => compose; 39 | configureStore(undefined, browserHistory); 40 | expect(compose).toHaveBeenCalled(); 41 | /* eslint-enable */ 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/app/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /frontend/app/utils/checkStore.js: -------------------------------------------------------------------------------- 1 | import conformsTo from 'lodash/conformsTo'; 2 | import isFunction from 'lodash/isFunction'; 3 | import isObject from 'lodash/isObject'; 4 | import invariant from 'invariant'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export default function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | injectedReducers: isObject, 17 | injectedSagas: isObject, 18 | }; 19 | invariant( 20 | conformsTo(store, shape), 21 | '(app/utils...) injectors: Expected a valid redux store', 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/app/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount'; 2 | export const DAEMON = '@@saga-injector/daemon'; 3 | export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount'; 4 | -------------------------------------------------------------------------------- /frontend/app/utils/injectReducer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import hoistNonReactStatics from 'hoist-non-react-statics'; 4 | 5 | import getInjectors from './reducerInjectors'; 6 | 7 | /** 8 | * Dynamically injects a reducer 9 | * 10 | * @param {string} key A key of the reducer 11 | * @param {function} reducer A reducer that will be injected 12 | * 13 | */ 14 | export default ({ key, reducer }) => WrappedComponent => { 15 | class ReducerInjector extends React.Component { 16 | static WrappedComponent = WrappedComponent; 17 | static contextTypes = { 18 | store: PropTypes.object.isRequired, 19 | }; 20 | static displayName = `withReducer(${WrappedComponent.displayName || 21 | WrappedComponent.name || 22 | 'Component'})`; 23 | 24 | componentWillMount() { 25 | const { injectReducer } = this.injectors; 26 | 27 | injectReducer(key, reducer); 28 | } 29 | 30 | injectors = getInjectors(this.context.store); 31 | 32 | render() { 33 | return ; 34 | } 35 | } 36 | 37 | return hoistNonReactStatics(ReducerInjector, WrappedComponent); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/app/utils/injectSaga.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import hoistNonReactStatics from 'hoist-non-react-statics'; 4 | 5 | import getInjectors from './sagaInjectors'; 6 | 7 | /** 8 | * Dynamically injects a saga, passes component's props as saga arguments 9 | * 10 | * @param {string} key A key of the saga 11 | * @param {function} saga A root saga that will be injected 12 | * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and 13 | * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options: 14 | * - constants.DAEMON—starts the saga on component mount and never cancels it or starts again, 15 | * - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again. 16 | * 17 | */ 18 | export default ({ key, saga, mode }) => WrappedComponent => { 19 | class InjectSaga extends React.Component { 20 | static WrappedComponent = WrappedComponent; 21 | static contextTypes = { 22 | store: PropTypes.object.isRequired, 23 | }; 24 | static displayName = `withSaga(${WrappedComponent.displayName || 25 | WrappedComponent.name || 26 | 'Component'})`; 27 | 28 | componentWillMount() { 29 | const { injectSaga } = this.injectors; 30 | 31 | injectSaga(key, { saga, mode }, this.props); 32 | } 33 | 34 | componentWillUnmount() { 35 | const { ejectSaga } = this.injectors; 36 | 37 | ejectSaga(key); 38 | } 39 | 40 | injectors = getInjectors(this.context.store); 41 | 42 | render() { 43 | return ; 44 | } 45 | } 46 | 47 | return hoistNonReactStatics(InjectSaga, WrappedComponent); 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/app/utils/middlewares.js: -------------------------------------------------------------------------------- 1 | import {isAccessTokenExpired} from "containers/AuthProvider/selectors"; 2 | import {NEW_ACCESS_TOKEN} from "containers/LoginPage/constants"; 3 | import {refreshAccessToken} from "./apiHandlers"; 4 | import axios from "axios/index"; 5 | 6 | export const logger = store => next => action => { 7 | console.log('dispatching', action); 8 | const result = next(action); 9 | console.log('next state', store.getState()); 10 | return result; 11 | }; 12 | 13 | export const checkAccessToken = (store) => next => action => { 14 | // console.log("checking the token status") 15 | // store.dispatch({type: NEW_ACCESS_TOKEN, new_token: 'fsdafasd'}) 16 | const token = store.getState().get('auth'); 17 | if(action.type !== NEW_ACCESS_TOKEN && token.get('access') !== undefined && isAccessTokenExpired(token.toJS())){ 18 | // if(action.type !== NEW_ACCESS_TOKEN && token.get('access') !== undefined){ 19 | // token is expired 20 | // get a new token using the refresh token 21 | // console.log(token.get('refresh').token) 22 | // TODO maybe there is a better way for doing asynchrous call? 23 | return refreshAccessToken(token.get('refresh').get('token'), 24 | (new_token) => {store.dispatch({type: NEW_ACCESS_TOKEN, new_token: new_token}); return next(action);}); 25 | // pass 26 | 27 | } 28 | return next(action); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/app/utils/reducerInjectors.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | import isFunction from 'lodash/isFunction'; 4 | import isString from 'lodash/isString'; 5 | 6 | import checkStore from './checkStore'; 7 | import createReducer from '../reducers'; 8 | 9 | export function injectReducerFactory(store, isValid) { 10 | return function injectReducer(key, reducer) { 11 | if (!isValid) checkStore(store); 12 | 13 | invariant( 14 | isString(key) && !isEmpty(key) && isFunction(reducer), 15 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function', 16 | ); 17 | 18 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different 19 | if ( 20 | Reflect.has(store.injectedReducers, key) && 21 | store.injectedReducers[key] === reducer 22 | ) 23 | return; 24 | 25 | store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign 26 | store.replaceReducer(createReducer(store.injectedReducers)); 27 | }; 28 | } 29 | 30 | export default function getInjectors(store) { 31 | checkStore(store); 32 | 33 | return { 34 | injectReducer: injectReducerFactory(store, true), 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/app/utils/tests/checkStore.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test injectors 3 | */ 4 | 5 | import checkStore from '../checkStore'; 6 | 7 | describe('checkStore', () => { 8 | let store; 9 | 10 | beforeEach(() => { 11 | store = { 12 | dispatch: () => {}, 13 | subscribe: () => {}, 14 | getState: () => {}, 15 | replaceReducer: () => {}, 16 | runSaga: () => {}, 17 | injectedReducers: {}, 18 | injectedSagas: {}, 19 | }; 20 | }); 21 | 22 | it('should not throw if passed valid store shape', () => { 23 | expect(() => checkStore(store)).not.toThrow(); 24 | }); 25 | 26 | it('should throw if passed invalid store shape', () => { 27 | expect(() => checkStore({})).toThrow(); 28 | expect(() => checkStore({ ...store, injectedSagas: null })).toThrow(); 29 | expect(() => checkStore({ ...store, injectedReducers: null })).toThrow(); 30 | expect(() => checkStore({ ...store, runSaga: null })).toThrow(); 31 | expect(() => checkStore({ ...store, replaceReducer: null })).toThrow(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /frontend/app/utils/tests/injectReducer.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test injectors 3 | */ 4 | 5 | import { memoryHistory } from 'react-router-dom'; 6 | import { shallow } from 'enzyme'; 7 | import React from 'react'; 8 | import identity from 'lodash/identity'; 9 | 10 | import configureStore from '../../configureStore'; 11 | import injectReducer from '../injectReducer'; 12 | import * as reducerInjectors from '../reducerInjectors'; 13 | 14 | // Fixtures 15 | const Component = () => null; 16 | 17 | const reducer = identity; 18 | 19 | describe('injectReducer decorator', () => { 20 | let store; 21 | let injectors; 22 | let ComponentWithReducer; 23 | 24 | beforeAll(() => { 25 | reducerInjectors.default = jest.fn().mockImplementation(() => injectors); 26 | }); 27 | 28 | beforeEach(() => { 29 | store = configureStore({}, memoryHistory); 30 | injectors = { 31 | injectReducer: jest.fn(), 32 | }; 33 | ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component); 34 | reducerInjectors.default.mockClear(); 35 | }); 36 | 37 | it('should inject a given reducer', () => { 38 | shallow(, { context: { store } }); 39 | 40 | expect(injectors.injectReducer).toHaveBeenCalledTimes(1); 41 | expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer); 42 | }); 43 | 44 | it('should set a correct display name', () => { 45 | expect(ComponentWithReducer.displayName).toBe('withReducer(Component)'); 46 | expect( 47 | injectReducer({ key: 'test', reducer })(() => null).displayName, 48 | ).toBe('withReducer(Component)'); 49 | }); 50 | 51 | it('should propagate props', () => { 52 | const props = { testProp: 'test' }; 53 | const renderedComponent = shallow(, { 54 | context: { store }, 55 | }); 56 | 57 | expect(renderedComponent.prop('testProp')).toBe('test'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /frontend/appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: 9 15 | - nodejs_version: 8 16 | 17 | # Fix line endings in Windows. (runs before repo cloning) 18 | init: 19 | - git config --global core.autocrlf input 20 | 21 | # Install scripts--runs after repo cloning 22 | install: 23 | # Install chrome 24 | - choco install -y googlechrome --ignore-checksums 25 | # Install the latest stable version of Node 26 | - ps: Install-Product node $env:nodejs_version 27 | - set PATH=%APPDATA%\yarn;%PATH% 28 | - yarn 29 | 30 | # Disable automatic builds 31 | build: off 32 | 33 | # Post-install test scripts 34 | test_script: 35 | # Output debugging info 36 | - node --version 37 | - node ./internals/scripts/generate-templates-for-linting 38 | # run tests and run build 39 | - yarn run test 40 | - yarn run build 41 | 42 | # Cache node_modules for faster builds 43 | cache: 44 | - "%LOCALAPPDATA%\\Yarn" 45 | - node_modules -> package.json 46 | 47 | # remove, as appveyor doesn't support secure variables on pr builds 48 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 49 | #on_success: 50 | #- yarn run coveralls 51 | -------------------------------------------------------------------------------- /frontend/docs/css/linting.md: -------------------------------------------------------------------------------- 1 | # Linting 2 | 3 | We use `stylelint` for CSS linting. `stylelint` catches syntax errors and helps you and your team stay consistent with modern CSS standards and conventions. 4 | 5 | We've pre-configured it specifically for the boilerplate's `styled-components` setup (and following that package's [recommendations](https://www.styled-components.com/docs/tooling#stylelint)). 6 | 7 | However, you can (and should!) adapt it to your own style setup if you decide to modify it. 8 | 9 | You can trigger `stylelint` using the `npm run lint:css` command or you can integrate with your IDE by downloading the relevant plugin. We recommend the latter. 10 | 11 | See the [official documentation](https://stylelint.io/) for more information! 12 | -------------------------------------------------------------------------------- /frontend/docs/css/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing `sanitize.css` 2 | 3 | To remove `sanitize.css` you will need to remove it from both: 4 | 5 | - [`app.js`](../../app/app.js) 6 | 7 | ```diff 8 | import FontFaceObserver from 'fontfaceobserver'; 9 | import { useScroll } from 'react-router-scroll'; 10 | -import 'sanitize.css/sanitize.css'; 11 | 12 | // Import root app 13 | import App from 'containers/App'; 14 | ``` 15 | 16 | - [`package.json`](../../package.json)! 17 | 18 | ```diff 19 | "dllPlugin": { 20 | "path": "node_modules/react-boilerplate-dlls", 21 | "exclude": [ 22 | ... 23 | "ip", 24 | "minimist", 25 | - "sanitize.css" 26 | ], 27 | }, 28 | ... 29 | "dependencies": { 30 | ... 31 | "redux-saga": "0.14.3", 32 | "reselect": "2.5.4", 33 | - "sanitize.css": "4.1.0", 34 | "styled-components": "1.4.3", 35 | ... 36 | }, 37 | ``` 38 | -------------------------------------------------------------------------------- /frontend/docs/css/sanitize.md: -------------------------------------------------------------------------------- 1 | # `sanitize.css` 2 | 3 | Sanitize.css makes browsers render elements more in 4 | line with developer expectations (e.g. having the box model set to a cascading 5 | `box-sizing: border-box`) and preferences (its defaults can be individually 6 | overridden). 7 | 8 | It was selected over older projects like `normalize.css` and `reset.css` due 9 | to its greater flexibility and better alignment with CSSNext features like CSS 10 | variables. 11 | 12 | See the [official documentation](https://github.com/10up/sanitize.css) for more 13 | information. 14 | 15 | --- 16 | 17 | _Don't like this feature? [Click here](remove.md)_ 18 | -------------------------------------------------------------------------------- /frontend/docs/general/editor.md: -------------------------------------------------------------------------------- 1 | # Setting Up Your Editor 2 | 3 | You can edit React Boilerplate using any editor or IDE, but there are a few extra steps that you can take to make sure your coding experience is as good as it can be. 4 | 5 | ## VS Code 6 | 7 | To get the best editing experience with [VS Code](https://code.visualstudio.com), create a [`jsconfig.json`](https://code.visualstudio.com/Docs/languages/javascript#_javascript-project-jsconfigjson) file at the root of your project: 8 | 9 | ```json 10 | { 11 | "compilerOptions": { 12 | "baseUrl": "app", 13 | "module": "commonjs", 14 | "target": "es2016", 15 | "jsx": "react" 16 | }, 17 | "exclude": ["node_modules", "**/node_modules/*"] 18 | } 19 | ``` 20 | 21 | This `jsconfig.json` file tells VS Code to treat all JS file as part of the same project, improving IntelliSense, code navigation, and refactoring. You can configure project wide settings in using the `jsconfig.json`, such as only allowing functions from the ES5 standard library, or even enable [more advanced type checking for JS files](https://code.visualstudio.com/docs/languages/javascript#_type-checking-and-quick-fixes-for-javascript-files) 22 | 23 | # ESLint + Prettier integration 24 | 25 | You can also get VSCode to understand your project's static code analysis setup. If you do this: 26 | 27 | - You'll see any warnings or errors directly within VSCode 28 | - VSCode can also automatically fix or format your code for you 29 | 30 | To make this happen, install both the ESLint and Prettier extensions for VSCode and add the following to either your User or Workspace Settings: 31 | 32 | ```json 33 | { 34 | "editor.formatOnSave": true, 35 | "prettier.eslintIntegration": true, 36 | "eslint.run": "onSave" 37 | } 38 | ``` 39 | 40 | Here's also a detailed video on the topic: [How to Setup VS Code + Prettier + ESLint](https://www.youtube.com/watch?v=YIvjKId9m2c) 41 | -------------------------------------------------------------------------------- /frontend/docs/general/files.md: -------------------------------------------------------------------------------- 1 | # Configuration: A Glossary 2 | 3 | A guide to the configuration files for this project: where they live and what 4 | they do. 5 | 6 | ## The root folder 7 | 8 | - `.editorconfig`: Sets the default configuration for certain files across editors. (e.g. indentation) 9 | 10 | - `.gitattributes`: Normalizes how `git`, the version control system this boilerplate uses, handles certain files. 11 | 12 | - `.gitignore`: Tells `git` to ignore certain files and folders which don't need to be version controlled, like the build folder. 13 | 14 | - `.travis.yml` and `appveyor.yml`: Continuous Integration configuration
15 | This boilerplate uses [Travis CI](https://travis-ci.com) for Linux environments 16 | and [AppVeyor](https://www.appveyor.com/) for Windows platforms, but feel free 17 | to swap either out for your own choice of CI. 18 | 19 | - `package.json`: Our `npm` configuration file has three functions: 20 | 21 | 1. It's where Babel and ESLint are configured 22 | 1. It's the API for the project: a consistent interface for all its controls 23 | 1. It lists the project's package dependencies 24 | 25 | Baking the config in is a slightly unusual set-up, but it allows us to keep 26 | the project root as uncluttered and grokkable-at-a-glance as possible. 27 | 28 | ## The `./internals` folder 29 | 30 | This is where the bulk of the tooling configuration lives, broken out into 31 | recognisable units of work. 32 | 33 | Feel free to change anything you like but don't be afraid to [ask upfront](https://gitter.im/mxstbr/react-boilerplate) 34 | whether you should: build systems are easy to break! 35 | -------------------------------------------------------------------------------- /frontend/docs/general/remove.md: -------------------------------------------------------------------------------- 1 | ### Removing offline access 2 | 3 | **Careful** about removing this, as there is no real downside to having your 4 | application available when the users network connection isn't perfect. 5 | 6 | To remove offline capability, delete the `offline-plugin` from the 7 | [`package.json`](../../package.json), remove the import of the plugin in 8 | [`app.js`](../../app/app.js) and remove the plugin from the 9 | [`webpack.prod.babel.js`](../../internals/webpack/webpack.prod.babel.js). 10 | 11 | ### Removing add to homescreen functionality 12 | 13 | Delete `webpack-pwa-manifest` from the the list of plugins in 14 | [`webpack.prod.babel.js`](../../internals/webpack/webpack.prod.babel.js). 15 | 16 | ### Removing performant web font loading 17 | 18 | **Careful** about removing this, as perceived performance might be highly impacted. 19 | 20 | To remove `FontFaceObserver`, don't import it in [`app.js`](../../app/app.js) and 21 | remove it from the [`package.json`](../../package.json). 22 | 23 | ### Removing image optimization 24 | 25 | To remove image optimization, delete the `image-webpack-loader` from the 26 | [`package.json`](../../package.json), and remove the `image-loader` from [`webpack.base.babel.js`](../../internals/webpack/webpack.base.babel.js): 27 | 28 | ``` 29 | … 30 | { 31 | test: /\.(jpg|png|gif)$/, 32 | loaders: [ 33 | 'file-loader', 34 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 35 | ], 36 | } 37 | … 38 | ``` 39 | 40 | Then replace it with classic `file-loader`: 41 | 42 | ``` 43 | … 44 | { 45 | test: /\.(jpg|png|gif)$/, 46 | loader: 'file-loader', 47 | } 48 | … 49 | ``` 50 | -------------------------------------------------------------------------------- /frontend/docs/general/server-configs.md: -------------------------------------------------------------------------------- 1 | # Server Configurations 2 | 3 | ## Apache 4 | 5 | This boilerplate includes an `app/.htaccess` file that does three things: 6 | 7 | 1. Redirect all traffic to HTTPS because ServiceWorker only works for encrypted 8 | traffic. 9 | 2. Rewrite all pages (e.g. `yourdomain.com/subpage`) to `yourdomain.com/index.html` 10 | to let `react-router` take care of presenting the correct page. 11 | 3. Ensure that sw.js is not cached. This is required for updates to be downloaded in offline-first mode. 12 | 13 | > Note: For performance reasons you should probably adapt this to run as a static 14 | > `.conf` file (typically under `/etc/apache2/sites-enabled` or similar) so that 15 | > your server doesn't have to apply these rules dynamically per request) 16 | 17 | ### security 18 | 19 | `.htaccess` can only provide security by redirecting HTTP to HTTPS 20 | 21 | > Note: For a detailled security configuration in apache httpd, a `.conf` file is necessary. You can use [Mozillas TLS Configurator](https://mozilla.github.io/server-side-tls/ssl-config-generator/) to get some examples. 22 | 23 | ## Nginx 24 | 25 | An `app/.nginx.conf` file is included that does the same on an Nginx server. 26 | 27 | ### security 28 | 29 | Additionally, the `.nginx.conf` provides TLS security configuration settings based on [Mozillas TLS Guidelines](https://wiki.mozilla.org/Security/Server_Side_TLS), including: 30 | 31 | - HSTS Header 32 | - TLS 1.2 only 33 | - Prefer server-side ciphersuites 34 | - Strong ciphersuites 35 | - Own DH Key (optional) 36 | - OCSP & SSL Stapling (optional) 37 | -------------------------------------------------------------------------------- /frontend/docs/general/webstorm-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/frontend/docs/general/webstorm-debug.png -------------------------------------------------------------------------------- /frontend/docs/general/webstorm-eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/frontend/docs/general/webstorm-eslint.png -------------------------------------------------------------------------------- /frontend/docs/general/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/createdbyfahad/django-react-example/5b93ec3e1f7234f35759b46f80fda9e7b9387406/frontend/docs/general/workflow.png -------------------------------------------------------------------------------- /frontend/docs/js/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 2 | 3 | ## State management 4 | 5 | This boilerplate manages application state using [Redux](redux.md), makes it 6 | immutable with [`ImmutableJS`](immutablejs.md) and keeps access performant 7 | via [`reselect`](reselect.md). 8 | 9 | For managing asynchronous flows (e.g. logging in) we use [`redux-saga`](redux-saga.md). 10 | 11 | For routing, we use [`react-router` in combination with `react-router-redux`](routing.md). 12 | 13 | We include a generator for components, containers, sagas, routes and selectors. 14 | Run `npm run generate` to choose from the available generators, and automatically 15 | add new parts of your application! 16 | 17 | > Note: If you want to skip the generator selection process, 18 | > `npm run generate ` also works. (e.g. `npm run generate container`) 19 | 20 | ### Learn more 21 | 22 | - [Redux](redux.md) 23 | - [ImmutableJS](immutablejs.md) 24 | - [reselect](reselect.md) 25 | - [redux-saga](redux-saga.md) 26 | - [react-intl](i18n.md) 27 | - [routing](routing.md) 28 | - [Asynchronously loaded components](async-components.md) 29 | 30 | ## Architecture: `components` and `containers` 31 | 32 | We adopted a split between stateless, reusable components called (wait for it...) 33 | `components` and stateful parent components called `containers`. 34 | 35 | ### Learn more 36 | 37 | See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 38 | by Dan Abramov for a great introduction to this approach. 39 | -------------------------------------------------------------------------------- /frontend/docs/js/async-components.md: -------------------------------------------------------------------------------- 1 | # Loading components with react-loadable 2 | 3 | [`react-loadable`](https://github.com/thejameskyle/react-loadable) is integrated into 4 | `react-boilerplate` because of its rich functionality and good design (it does not 5 | conflate routes with asynchronous components). 6 | 7 | To load component asynchronously, create a `Loadable` file by hand or via component/container generators with 8 | 'Do you want an async loader?' option activated. This is how it can look like: 9 | 10 | ```JS 11 | import Loadable from 'react-loadable'; 12 | 13 | import LoadingIndicator from 'components/LoadingIndicator'; 14 | 15 | export default Loadable({ 16 | loader: () => import('./index'), 17 | loading: LoadingIndicator, 18 | }); 19 | ``` 20 | 21 | You can find more information on how to use `react-loadable` in [their docs](https://github.com/thejameskyle/react-loadable). 22 | -------------------------------------------------------------------------------- /frontend/docs/js/reselect.md: -------------------------------------------------------------------------------- 1 | # `reselect` 2 | 3 | reselect memoizes ("caches") previous state trees and calculations based on said 4 | tree. This means repeated changes and calculations are fast and efficient, 5 | providing us with a performance boost over standard `mapStateToProps` 6 | implementations. 7 | 8 | The [official documentation](https://github.com/reactjs/reselect) 9 | offers a good starting point! 10 | 11 | ## Usage 12 | 13 | There are two different kinds of selectors, simple and complex ones. 14 | 15 | ### Simple selectors 16 | 17 | Simple selectors are just that: they take the application state and select a 18 | part of it. 19 | 20 | ```javascript 21 | const mySelector = state => state.get('someState'); 22 | 23 | export { mySelector }; 24 | ``` 25 | 26 | ### Complex selectors 27 | 28 | If we need to, we can combine simple selectors to build more complex ones which 29 | get nested state parts with reselect's `createSelector` function. We import other 30 | selectors and pass them to the `createSelector` call: 31 | 32 | ```javascript 33 | import { createSelector } from 'reselect'; 34 | import mySelector from 'mySelector'; 35 | 36 | const myComplexSelector = createSelector(mySelector, myState => 37 | myState.get('someNestedState') 38 | ); 39 | 40 | export { myComplexSelector }; 41 | ``` 42 | 43 | These selectors can then either be used directly in our containers as 44 | `mapStateToProps` functions or be nested with `createSelector` once again: 45 | 46 | ```javascript 47 | export default connect( 48 | createSelector(myComplexSelector, myNestedState => ({ data: myNestedState })) 49 | )(SomeComponent); 50 | ``` 51 | 52 | ### Adding a new selector 53 | 54 | If you have a `selectors.js` file next to the reducer which's part of the state 55 | you want to select, add your selector to said file. If you don't have one yet, 56 | add a new one into your container folder and fill it with this boilerplate code: 57 | 58 | ```JS 59 | import { createSelector } from 'reselect'; 60 | 61 | const selectMyState = () => createSelector( 62 | 63 | ); 64 | 65 | export { 66 | selectMyState, 67 | }; 68 | ``` 69 | 70 | --- 71 | 72 | _Don't like this feature? [Click here](remove.md)_ 73 | -------------------------------------------------------------------------------- /frontend/docs/js/routing.md: -------------------------------------------------------------------------------- 1 | # Routing via `react-router` and `react-router-redux` 2 | 3 | `react-router` is the de-facto standard routing solution for react applications. 4 | The thing is that with redux and a single state tree, the URL is part of that 5 | state. `react-router-redux` takes care of synchronizing the location of our 6 | application with the application state. 7 | 8 | (See the [`react-router-redux` documentation](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux) 9 | for more information) 10 | 11 | ## Usage 12 | 13 | To add a new route, simply import the `Route` component and use it standalone or inside the `Switch` component (all part of [RR4 API](https://reacttraining.com/react-router/web/api)): 14 | 15 | ```JS 16 | 17 | ``` 18 | 19 | Top level routes are located in `App.js`. 20 | 21 | If you want your route component (or any component for that matter) to be loaded asynchronously, use container or component generator with 'Do you want an async loader?' option activated. 22 | 23 | To go to a new page use the `push` function by `react-router-redux`: 24 | 25 | ```JS 26 | import { push } from 'react-router-redux'; 27 | 28 | dispatch(push('/some/page')); 29 | ``` 30 | 31 | ## Child Routes 32 | 33 | For example, if you have a route called `about` at `/about` and want to make a child route called `team` at `/about/our-team`, follow the example 34 | in `App.js` to create a `Switch` within the parent component. Also remove the `exact` property from the `about` parent route. 35 | 36 | ```JS 37 | // AboutPage/index.js 38 | import { Switch, Route } from 'react-router-dom'; 39 | 40 | class AboutPage extends React.PureComponent { 41 | render() { 42 | return ( 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | ``` 50 | 51 | Note that with React Router v4, route re-rendering is handled by React's `setState`. This 52 | means that when wrapping route components in a redux connected container, or `PureComponent` or any other component with 53 | `shouldComponentUpdate`, you need to create a [ConnectedSwitch](https://github.com/ReactTraining/react-router/issues/5072#issuecomment-310184271) 54 | container that receives `location` directly from a redux store. Read more about this in 55 | [Dealing with Update Blocking](https://reacttraining.com/react-router/web/guides/dealing-with-update-blocking). 56 | 57 | You can read more in [`react-router`'s documentation](https://reacttraining.com/react-router/web/api). 58 | -------------------------------------------------------------------------------- /frontend/docs/testing/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | - [Unit Testing](unit-testing.md) 4 | - [Component Testing](component-testing.md) 5 | - [Remote Testing](remote-testing.md) 6 | 7 | Testing your application is a vital part of serious development. There are a few 8 | things you should test. If you've never done this before start with [unit testing](unit-testing.md). 9 | Move on to [component testing](component-testing.md) when you feel like you 10 | understand that! 11 | 12 | We also support [remote testing](remote-testing.md) your local application, 13 | which is quite awesome, so definitely check that out! 14 | 15 | ## Usage with this boilerplate 16 | 17 | To test your application started with this boilerplate do the following: 18 | 19 | 1. Sprinkle `.test.js` files directly next to the parts of your application you 20 | want to test. (Or in `test/` subdirectories, it doesn't really matter as long 21 | as they are directly next to those parts and end in `.test.js`) 22 | 23 | 1. Write your unit and component tests in those files. 24 | 25 | 1. Run `npm run test` in your terminal and see all the tests pass! (hopefully) 26 | 27 | There are a few more commands related to testing, checkout the [commands documentation](../general/commands.md#testing) 28 | for the full list! 29 | -------------------------------------------------------------------------------- /frontend/docs/testing/remote-testing.md: -------------------------------------------------------------------------------- 1 | # Remote testing 2 | 3 | ```Shell 4 | npm run start:tunnel 5 | ``` 6 | 7 | This command will start a server and tunnel it with `ngrok`. You'll get a URL 8 | that looks a bit like this: `http://abcdef.ngrok.com` 9 | 10 | This URL will show the version of your application that's in the `build` folder, 11 | and it's accessible from the entire world! This is great for testing on different 12 | devices and from different locations! 13 | -------------------------------------------------------------------------------- /frontend/internals/config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const pullAll = require('lodash/pullAll'); 3 | const uniq = require('lodash/uniq'); 4 | 5 | const ReactBoilerplate = { 6 | // This refers to the react-boilerplate version this project is based on. 7 | version: '3.6.0', 8 | 9 | /** 10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading 11 | * by caching the module metadata for all of our npm dependencies. We enable it by default 12 | * in development. 13 | * 14 | * 15 | * To disable the DLL Plugin, set this value to false. 16 | */ 17 | dllPlugin: { 18 | defaults: { 19 | /** 20 | * we need to exclude dependencies which are not intended for the browser 21 | * by listing them here. 22 | */ 23 | exclude: [ 24 | 'chalk', 25 | 'compression', 26 | 'cross-env', 27 | 'express', 28 | 'ip', 29 | 'minimist', 30 | 'sanitize.css', 31 | ], 32 | 33 | /** 34 | * Specify any additional dependencies here. We include core-js and lodash 35 | * since a lot of our dependencies depend on them and they get picked up by webpack. 36 | */ 37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], 38 | 39 | // The path where the DLL manifest and bundle will get built 40 | path: resolve('../node_modules/react-boilerplate-dlls'), 41 | }, 42 | 43 | entry(pkg) { 44 | const dependencyNames = Object.keys(pkg.dependencies); 45 | const exclude = 46 | pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 47 | const include = 48 | pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 49 | const includeDependencies = uniq(dependencyNames.concat(include)); 50 | 51 | return { 52 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 53 | }; 54 | }, 55 | }, 56 | }; 57 | 58 | module.exports = ReactBoilerplate; 59 | -------------------------------------------------------------------------------- /frontend/internals/generators/component/class.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | // import styled from 'styled-components'; 10 | 11 | {{#if wantMessages}} 12 | import { FormattedMessage } from 'react-intl'; 13 | import messages from './messages'; 14 | {{/if}} 15 | 16 | /* eslint-disable react/prefer-stateless-function */ 17 | class {{ properCase name }} extends {{{ type }}} { 18 | render() { 19 | return ( 20 |
21 | {{#if wantMessages}} 22 | 23 | {{/if}} 24 |
25 | ); 26 | } 27 | } 28 | 29 | {{ properCase name }}.propTypes = {}; 30 | 31 | export default {{ properCase name }}; 32 | -------------------------------------------------------------------------------- /frontend/internals/generators/component/loadable.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for {{ properCase name }} 4 | * 5 | */ 6 | 7 | import Loadable from 'react-loadable'; 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/internals/generators/component/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{ properCase name }} Messages 3 | * 4 | * This contains all the text for the {{ properCase name }} component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.components.{{ properCase name }}.header', 12 | defaultMessage: 'This is the {{ properCase name}} component !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | // import PropTypes from 'prop-types'; 9 | // import styled from 'styled-components'; 10 | 11 | {{#if wantMessages}} 12 | import { FormattedMessage } from 'react-intl'; 13 | import messages from './messages'; 14 | {{/if}} 15 | 16 | function {{ properCase name }}() { 17 | return ( 18 |
19 | {{#if wantMessages}} 20 | 21 | {{/if}} 22 |
23 | ); 24 | } 25 | 26 | {{ properCase name }}.propTypes = {}; 27 | 28 | export default {{ properCase name }}; 29 | -------------------------------------------------------------------------------- /frontend/internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import {{ properCase name }} from '../index'; 5 | 6 | describe('<{{ properCase name }} />', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { DEFAULT_ACTION } from './constants'; 8 | 9 | export function defaultAction() { 10 | return { 11 | type: DEFAULT_ACTION, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import { defaultAction } from '../actions'; 2 | import { DEFAULT_ACTION } from '../constants'; 3 | 4 | describe('{{ properCase name }} actions', () => { 5 | describe('Default Action', () => { 6 | it('has a type of DEFAULT_ACTION', () => { 7 | const expected = { 8 | type: DEFAULT_ACTION, 9 | }; 10 | expect(defaultAction()).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/class.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | {{#if wantHeaders}} 11 | import { Helmet } from 'react-helmet'; 12 | {{/if}} 13 | {{#if wantMessages}} 14 | import { FormattedMessage } from 'react-intl'; 15 | {{/if}} 16 | {{#if wantActionsAndReducer}} 17 | import { createStructuredSelector } from 'reselect'; 18 | {{/if}} 19 | import { compose } from 'redux'; 20 | 21 | {{#if wantSaga}} 22 | import injectSaga from 'utils/injectSaga'; 23 | {{/if}} 24 | {{#if wantActionsAndReducer}} 25 | import injectReducer from 'utils/injectReducer'; 26 | import makeSelect{{properCase name}} from './selectors'; 27 | import reducer from './reducer'; 28 | {{/if}} 29 | {{#if wantSaga}} 30 | import saga from './saga'; 31 | {{/if}} 32 | {{#if wantMessages}} 33 | import messages from './messages'; 34 | {{/if}} 35 | 36 | /* eslint-disable react/prefer-stateless-function */ 37 | export class {{ properCase name }} extends {{{ type }}} { 38 | render() { 39 | return ( 40 |
41 | {{#if wantHeaders}} 42 | 43 | {{properCase name}} 44 | 45 | 46 | {{/if}} 47 | {{#if wantMessages}} 48 | 49 | {{/if}} 50 |
51 | ); 52 | } 53 | } 54 | 55 | {{ properCase name }}.propTypes = { 56 | dispatch: PropTypes.func.isRequired, 57 | }; 58 | 59 | {{#if wantActionsAndReducer}} 60 | const mapStateToProps = createStructuredSelector({ 61 | {{ lowerCase name }}: makeSelect{{properCase name}}(), 62 | }); 63 | {{/if}} 64 | 65 | function mapDispatchToProps(dispatch) { 66 | return { 67 | dispatch, 68 | }; 69 | } 70 | 71 | {{#if wantActionsAndReducer}} 72 | const withConnect = connect( 73 | mapStateToProps, 74 | mapDispatchToProps 75 | ); 76 | 77 | const withReducer = injectReducer({ key: '{{ camelCase name }}', reducer }); 78 | {{else}} 79 | const withConnect = connect(null, mapDispatchToProps); 80 | {{/if}} 81 | {{#if wantSaga}} 82 | const withSaga = injectSaga({ key: '{{ camelCase name }}', saga }); 83 | {{/if}} 84 | 85 | export default compose( 86 | {{#if wantActionsAndReducer}} 87 | withReducer, 88 | {{/if}} 89 | {{#if wantSaga}} 90 | withSaga, 91 | {{/if}} 92 | withConnect 93 | )({{ properCase name }}); 94 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | {{#if wantHeaders}} 11 | import { Helmet } from 'react-helmet'; 12 | {{/if}} 13 | {{#if wantMessages}} 14 | import { FormattedMessage } from 'react-intl'; 15 | {{/if}} 16 | {{#if wantActionsAndReducer}} 17 | import { createStructuredSelector } from 'reselect'; 18 | import makeSelect{{properCase name}} from './selectors'; 19 | {{/if}} 20 | {{#if wantMessages}} 21 | import messages from './messages'; 22 | {{/if}} 23 | 24 | /* eslint-disable react/prefer-stateless-function */ 25 | export class {{ properCase name }} extends React.{{{ component }}} { 26 | render() { 27 | return ( 28 |
29 | {{#if wantHeaders}} 30 | 31 | {{properCase name}} 32 | 36 | 37 | {{/if}} 38 | {{#if wantMessages}} 39 | 40 | {{/if}} 41 |
42 | ); 43 | } 44 | } 45 | 46 | {{ properCase name }}.propTypes = { 47 | dispatch: PropTypes.func.isRequired, 48 | }; 49 | 50 | {{#if wantActionsAndReducer}} 51 | const mapStateToProps = createStructuredSelector({ 52 | {{name}}: makeSelect{{properCase name}}(), 53 | }); 54 | {{/if}} 55 | 56 | function mapDispatchToProps(dispatch) { 57 | return { 58 | dispatch, 59 | }; 60 | } 61 | 62 | {{#if wantActionsAndReducer}} 63 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 64 | {{else}} 65 | export default connect(null, mapDispatchToProps)({{ properCase name }}); 66 | {{/if}} 67 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{properCase name }} Messages 3 | * 4 | * This contains all the text for the {{properCase name }} component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: 'app.containers.{{properCase name }}.header', 12 | defaultMessage: 'This is {{properCase name}} container !', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { DEFAULT_ACTION } from './constants'; 9 | 10 | export const initialState = fromJS({}); 11 | 12 | function {{ camelCase name }}Reducer(state = initialState, action) { 13 | switch (action.type) { 14 | case DEFAULT_ACTION: 15 | return state; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default {{ camelCase name }}Reducer; 22 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | 4 | describe('{{ camelCase name }}Reducer', () => { 5 | it('returns the initial state', () => { 6 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/saga.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export default function* defaultSaga() { 5 | // See example in containers/HomePage/saga.js 6 | } 7 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/saga.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the {{ camelCase name }} state domain 6 | */ 7 | 8 | const select{{ properCase name }}Domain = state => 9 | state.get('{{ camelCase name }}', initialState); 10 | 11 | /** 12 | * Other specific selectors 13 | */ 14 | 15 | /** 16 | * Default selector used by {{ properCase name }} 17 | */ 18 | 19 | const makeSelect{{ properCase name }} = () => 20 | createSelector(select{{ properCase name }}Domain, substate => substate.toJS()); 21 | 22 | export default makeSelect{{ properCase name }}; 23 | export { select{{ properCase name }}Domain }; 24 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { select{{ properCase name }}Domain } from '../selectors'; 3 | 4 | describe('select{{ properCase name }}Domain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | {{#if wantHeaders}} 11 | import { Helmet } from 'react-helmet'; 12 | {{/if}} 13 | {{#if wantMessages}} 14 | import { FormattedMessage } from 'react-intl'; 15 | {{/if}} 16 | {{#if wantActionsAndReducer}} 17 | import { createStructuredSelector } from 'reselect'; 18 | {{/if}} 19 | import { compose } from 'redux'; 20 | 21 | {{#if wantSaga}} 22 | import injectSaga from 'utils/injectSaga'; 23 | {{/if}} 24 | {{#if wantActionsAndReducer}} 25 | import injectReducer from 'utils/injectReducer'; 26 | import makeSelect{{properCase name}} from './selectors'; 27 | import reducer from './reducer'; 28 | {{/if}} 29 | {{#if wantSaga}} 30 | import saga from './saga'; 31 | {{/if}} 32 | {{#if wantMessages}} 33 | import messages from './messages'; 34 | {{/if}} 35 | 36 | function {{ properCase name }}() { 37 | return ( 38 |
39 | {{#if wantHeaders}} 40 | 41 | {{properCase name}} 42 | 43 | 44 | {{/if}} 45 | {{#if wantMessages}} 46 | 47 | {{/if}} 48 |
49 | ); 50 | } 51 | 52 | {{ properCase name }}.propTypes = { 53 | dispatch: PropTypes.func.isRequired, 54 | }; 55 | 56 | {{#if wantActionsAndReducer}} 57 | const mapStateToProps = createStructuredSelector({ 58 | {{ lowerCase name }}: makeSelect{{properCase name}}(), 59 | }); 60 | {{/if}} 61 | 62 | function mapDispatchToProps(dispatch) { 63 | return { 64 | dispatch, 65 | }; 66 | } 67 | 68 | {{#if wantActionsAndReducer}} 69 | const withConnect = connect(mapStateToProps, mapDispatchToProps); 70 | 71 | const withReducer = injectReducer({ key: '{{ lowerCase name }}', reducer }); 72 | {{else}} 73 | const withConnect = connect(null, mapDispatchToProps); 74 | {{/if}} 75 | {{#if wantSaga}} 76 | const withSaga = injectSaga({ key: '{{ lowerCase name }}', saga }); 77 | {{/if}} 78 | 79 | export default compose( 80 | {{#if wantActionsAndReducer}} 81 | withReducer, 82 | {{/if}} 83 | {{#if wantSaga}} 84 | withSaga, 85 | {{/if}} 86 | withConnect, 87 | )({{ properCase name }}); 88 | -------------------------------------------------------------------------------- /frontend/internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { {{ properCase name }} } from '../index'; 5 | 6 | describe('<{{ properCase name }} />', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const { exec } = require('child_process'); 10 | const componentGenerator = require('./component/index.js'); 11 | const containerGenerator = require('./container/index.js'); 12 | const languageGenerator = require('./language/index.js'); 13 | 14 | module.exports = plop => { 15 | plop.setGenerator('component', componentGenerator); 16 | plop.setGenerator('container', containerGenerator); 17 | plop.setGenerator('language', languageGenerator); 18 | plop.addHelper('directory', comp => { 19 | try { 20 | fs.accessSync( 21 | path.join(__dirname, `../../app/containers/${comp}`), 22 | fs.F_OK, 23 | ); 24 | return `containers/${comp}`; 25 | } catch (e) { 26 | return `components/${comp}`; 27 | } 28 | }); 29 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 30 | plop.setActionType('prettify', (answers, config) => { 31 | const folderPath = `${path.join( 32 | __dirname, 33 | '/../../app/', 34 | config.path, 35 | plop.getHelper('properCase')(answers.name), 36 | '**.js', 37 | )}`; 38 | exec(`npm run prettify -- "${folderPath}"`); 39 | return folderPath; 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 '{{language}}', 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $&const {{language}}LocaleData = require('react-intl/locale-data/{{language}}'); 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1const {{language}}TranslationMessages = require('./translations/{{language}}.json'); 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /frontend/internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const pageComponents = fs.readdirSync( 10 | path.join(__dirname, '../../../app/components'), 11 | ); 12 | const pageContainers = fs.readdirSync( 13 | path.join(__dirname, '../../../app/containers'), 14 | ); 15 | const components = pageComponents.concat(pageContainers); 16 | 17 | function componentExists(comp) { 18 | return components.indexOf(comp) >= 0; 19 | } 20 | 21 | module.exports = componentExists; 22 | -------------------------------------------------------------------------------- /frontend/internals/mocks/cssModule.js: -------------------------------------------------------------------------------- 1 | module.exports = 'CSS_MODULE'; 2 | -------------------------------------------------------------------------------- /frontend/internals/mocks/image.js: -------------------------------------------------------------------------------- 1 | module.exports = 'IMAGE_MOCK'; 2 | -------------------------------------------------------------------------------- /frontend/internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shelljs = require('shelljs'); 4 | const animateProgress = require('./helpers/progress'); 5 | const chalk = require('chalk'); 6 | const addCheckMark = require('./helpers/checkmark'); 7 | 8 | const progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback), // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + 21 | chalk.magenta('http://webpack.github.io/analyse/') + 22 | ' in your browser and upload the stats.json file!' + 23 | chalk.blue( 24 | '\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n', 25 | ), 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const addCheckMark = require('./helpers/checkmark.js'); 3 | 4 | if (!shell.which('git')) { 5 | shell.echo('Sorry, this script requires git'); 6 | shell.exit(1); 7 | } 8 | 9 | if (!shell.test('-e', 'internals/templates')) { 10 | shell.echo('The example is deleted already.'); 11 | shell.exit(1); 12 | } 13 | 14 | process.stdout.write('Cleanup started...'); 15 | 16 | // Reuse existing LanguageProvider and i18n tests 17 | shell.mv( 18 | 'app/containers/LanguageProvider/tests', 19 | 'internals/templates/containers/LanguageProvider', 20 | ); 21 | shell.cp('app/tests/i18n.test.js', 'internals/templates/tests/i18n.test.js'); 22 | 23 | // Cleanup components/ 24 | shell.rm('-rf', 'app/components/*'); 25 | 26 | // Handle containers/ 27 | shell.rm('-rf', 'app/containers'); 28 | shell.mv('internals/templates/containers', 'app'); 29 | 30 | // Handle tests/ 31 | shell.mv('internals/templates/tests', 'app'); 32 | 33 | // Handle translations/ 34 | shell.rm('-rf', 'app/translations'); 35 | shell.mv('internals/templates/translations', 'app'); 36 | 37 | // Handle utils/ 38 | shell.rm('-rf', 'app/utils'); 39 | shell.mv('internals/templates/utils', 'app'); 40 | 41 | // Replace the files in the root app/ folder 42 | shell.cp('internals/templates/app.js', 'app/app.js'); 43 | shell.cp('internals/templates/global-styles.js', 'app/global-styles.js'); 44 | shell.cp('internals/templates/i18n.js', 'app/i18n.js'); 45 | shell.cp('internals/templates/index.html', 'app/index.html'); 46 | shell.cp('internals/templates/reducers.js', 'app/reducers.js'); 47 | shell.cp('internals/templates/configureStore.js', 'app/configureStore.js'); 48 | 49 | // Remove the templates folder 50 | shell.rm('-rf', 'internals/templates'); 51 | 52 | addCheckMark(); 53 | 54 | // Commit the changes 55 | if ( 56 | shell.exec('git add . --all && git commit -qm "Remove default example"') 57 | .code !== 0 58 | ) { 59 | shell.echo('\nError: Git commit failed'); 60 | shell.exit(1); 61 | } 62 | 63 | shell.echo('\nCleanup done. Happy Coding!!!'); 64 | -------------------------------------------------------------------------------- /frontend/internals/scripts/dependencies.js: -------------------------------------------------------------------------------- 1 | // No need to build the DLL in production 2 | if (process.env.NODE_ENV === 'production') { 3 | process.exit(0); 4 | } 5 | 6 | require('shelljs/global'); 7 | 8 | const path = require('path'); 9 | const fs = require('fs'); 10 | const exists = fs.existsSync; 11 | const writeFile = fs.writeFileSync; 12 | 13 | const defaults = require('lodash/defaultsDeep'); 14 | const pkg = require(path.join(process.cwd(), 'package.json')); 15 | const config = require('../config'); 16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults); 17 | const outputPath = path.join(process.cwd(), dllConfig.path); 18 | const dllManifestPath = path.join(outputPath, 'package.json'); 19 | 20 | /** 21 | * I use node_modules/react-boilerplate-dlls by default just because 22 | * it isn't going to be version controlled and babel wont try to parse it. 23 | */ 24 | mkdir('-p', outputPath); 25 | 26 | echo('Building the Webpack DLL...'); 27 | 28 | /** 29 | * Create a manifest so npm install doesn't warn us 30 | */ 31 | if (!exists(dllManifestPath)) { 32 | writeFile( 33 | dllManifestPath, 34 | JSON.stringify( 35 | defaults({ 36 | name: 'react-boilerplate-dlls', 37 | private: true, 38 | author: pkg.author, 39 | repository: pkg.repository, 40 | version: pkg.version, 41 | }), 42 | null, 43 | 2, 44 | ), 45 | 'utf8', 46 | ); 47 | } 48 | 49 | // the BUILDING_DLL env var is set to avoid confusing the development environment 50 | exec( 51 | 'cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js --hide-modules', 52 | ); 53 | -------------------------------------------------------------------------------- /frontend/internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /frontend/internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const readline = require('readline'); 4 | 5 | /** 6 | * Adds an animated progress indicator 7 | * 8 | * @param {string} message The message to write next to the indicator 9 | * @param {number} amountOfDots The amount of dots you want to animate 10 | */ 11 | function animateProgress(message, amountOfDots) { 12 | if (typeof amountOfDots !== 'number') { 13 | amountOfDots = 3; 14 | } 15 | 16 | let i = 0; 17 | return setInterval(function() { 18 | readline.cursorTo(process.stdout, 0); 19 | i = (i + 1) % (amountOfDots + 1); 20 | const dots = new Array(i + 1).join('.'); 21 | process.stdout.write(message + dots); 22 | }, 500); 23 | } 24 | 25 | module.exports = animateProgress; 26 | -------------------------------------------------------------------------------- /frontend/internals/scripts/helpers/xmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark cross symbol 5 | */ 6 | function addXMark(callback) { 7 | process.stdout.write(chalk.red(' ✘')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addXMark; 12 | -------------------------------------------------------------------------------- /frontend/internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | exec('npm -v', function(err, stdout, stderr) { 3 | if (err) throw err; 4 | if (parseFloat(stdout) < 3) { 5 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 6 | process.exit(1); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/internals/testing/enzyme-setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /frontend/internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | -------------------------------------------------------------------------------- /frontend/internals/webpack/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WEBPACK DLL GENERATOR 3 | * 4 | * This profile is used to cache webpack's module 5 | * contexts for external library and framework type 6 | * dependencies which will usually not change often enough 7 | * to warrant building them from scratch every time we use 8 | * the webpack process. 9 | */ 10 | 11 | const { join } = require('path'); 12 | const defaults = require('lodash/defaultsDeep'); 13 | const webpack = require('webpack'); 14 | const pkg = require(join(process.cwd(), 'package.json')); 15 | const { dllPlugin } = require('../config'); 16 | 17 | if (!pkg.dllPlugin) { 18 | process.exit(0); 19 | } 20 | 21 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 22 | const outputPath = join(process.cwd(), dllConfig.path); 23 | 24 | module.exports = require('./webpack.base.babel')({ 25 | mode: 'development', 26 | context: process.cwd(), 27 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 28 | optimization: { 29 | minimize: false, 30 | }, 31 | devtool: 'eval', 32 | output: { 33 | filename: '[name].dll.js', 34 | path: outputPath, 35 | library: '[name]', 36 | }, 37 | plugins: [ 38 | new webpack.DllPlugin({ 39 | name: '[name]', 40 | path: join(outputPath, '[name].json'), 41 | }), 42 | ], 43 | performance: { 44 | hints: false, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /frontend/server/argv.js: -------------------------------------------------------------------------------- 1 | module.exports = require('minimist')(process.argv.slice(2)); 2 | -------------------------------------------------------------------------------- /frontend/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | const proxy = require('http-proxy-middleware'); 6 | 7 | const argv = require('./argv'); 8 | const port = require('./port'); 9 | const setup = require('./middlewares/frontendMiddleware'); 10 | const isDev = process.env.NODE_ENV !== 'production'; 11 | const ngrok = 12 | (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel 13 | ? require('ngrok') 14 | : false; 15 | const { resolve } = require('path'); 16 | const app = express(); 17 | 18 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 19 | const myApi = proxy('/api', { target: 'http://localhost:8000'}) 20 | app.use('/api', myApi); 21 | app.use('/media', proxy('/media', { target: 'http://localhost:8000'})); 22 | 23 | 24 | // In production we need to pass these values in instead of relying on webpack 25 | setup(app, { 26 | outputPath: resolve(process.cwd(), 'build'), 27 | publicPath: '/', 28 | }); 29 | 30 | // get the intended host and port number, use localhost and port 3000 if not provided 31 | const customHost = argv.host || process.env.HOST; 32 | const host = customHost || null; // Let http.Server use its default IPv6/4 host 33 | const prettyHost = customHost || 'localhost'; 34 | 35 | // Start your app. 36 | app.listen(port, host, async err => { 37 | if (err) { 38 | return logger.error(err.message); 39 | } 40 | 41 | // Connect to ngrok in dev mode 42 | if (ngrok) { 43 | let url; 44 | try { 45 | url = await ngrok.connect(port); 46 | } catch (e) { 47 | return logger.error(e); 48 | } 49 | logger.appStarted(port, prettyHost, url); 50 | } else { 51 | logger.appStarted(port, prettyHost); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /frontend/server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | // Called whenever there's an error on the server we want to print 13 | error: err => { 14 | console.error(chalk.red(err)); 15 | }, 16 | 17 | // Called when express.js app starts on given port w/o errors 18 | appStarted: (port, host, tunnelStarted) => { 19 | console.log(`Server started ! ${chalk.green('✓')}`); 20 | 21 | // If the tunnel started, log that and the URL it's available at 22 | if (tunnelStarted) { 23 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 24 | } 25 | 26 | console.log(` 27 | ${chalk.bold('Access URLs:')}${divider} 28 | Localhost: ${chalk.magenta(`http://${host}:${port}`)} 29 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 30 | (tunnelStarted 31 | ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` 32 | : '')}${divider} 33 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 34 | `); 35 | }, 36 | }; 37 | 38 | module.exports = logger; 39 | -------------------------------------------------------------------------------- /frontend/server/middlewares/addDevMiddlewares.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const webpackDevMiddleware = require('webpack-dev-middleware'); 4 | const webpackHotMiddleware = require('webpack-hot-middleware'); 5 | 6 | function createWebpackMiddleware(compiler, publicPath) { 7 | return webpackDevMiddleware(compiler, { 8 | logLevel: 'warn', 9 | publicPath, 10 | silent: true, 11 | stats: 'errors-only', 12 | }); 13 | } 14 | 15 | module.exports = function addDevMiddlewares(app, webpackConfig) { 16 | const compiler = webpack(webpackConfig); 17 | const middleware = createWebpackMiddleware( 18 | compiler, 19 | webpackConfig.output.publicPath, 20 | ); 21 | 22 | app.use(middleware); 23 | app.use(webpackHotMiddleware(compiler)); 24 | 25 | // Since webpackDevMiddleware uses memory-fs internally to store build 26 | // artifacts, we use it instead 27 | const fs = middleware.fileSystem; 28 | 29 | app.get('*', (req, res) => { 30 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 31 | if (err) { 32 | res.sendStatus(404); 33 | } else { 34 | res.send(file.toString()); 35 | } 36 | }); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/server/middlewares/addProdMiddlewares.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const compression = require('compression'); 4 | 5 | module.exports = function addProdMiddlewares(app, options) { 6 | const publicPath = options.publicPath || '/'; 7 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 8 | 9 | // compression middleware compresses your server responses which makes them 10 | // smaller (applies also to assets). You can read more about that technique 11 | // and other good practices on official Express.js docs http://mxs.is/googmy 12 | app.use(compression()); 13 | app.use(publicPath, express.static(outputPath)); 14 | 15 | app.get('*', (req, res) => 16 | res.sendFile(path.resolve(outputPath, 'index.html')), 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | /** 4 | * Front-end middleware 5 | */ 6 | module.exports = (app, options) => { 7 | const isProd = process.env.NODE_ENV === 'production'; 8 | 9 | if (isProd) { 10 | const addProdMiddlewares = require('./addProdMiddlewares'); 11 | addProdMiddlewares(app, options); 12 | } else { 13 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 14 | const addDevMiddlewares = require('./addDevMiddlewares'); 15 | addDevMiddlewares(app, webpackConfig); 16 | } 17 | 18 | return app; 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/server/port.js: -------------------------------------------------------------------------------- 1 | const argv = require('./argv'); 2 | 3 | module.exports = parseInt(argv.port || process.env.PORT || '3000', 10); 4 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Todo: 4 | - DONE persist auth info for user login/logout 5 | - add support for tags 6 | - 7 | 8 | 9 | 10 | Comments: 11 | --------------------------------------------------------------------------------