├── .editorconfig ├── .eslintrc ├── .gitignore ├── Procfile ├── README.md ├── coldoutreach ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── generator ├── __init__.py ├── admin.py ├── apps.py ├── fixtures │ ├── Christopher_Salat_Ceev.pdf │ ├── DanBernierProfile.pdf │ ├── DylanHirschkornProfile.pdf │ ├── SeanDuganMurphyProfile.pdf │ ├── TariqAliProfile.pdf │ └── Tariq_Ali.pdf ├── letter_generator.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── resume_parser.py ├── resume_saver.py ├── technology_jargon.py ├── templates │ └── generator │ │ ├── email.html │ │ ├── index.html │ │ └── profile.html ├── tests.py ├── urls.py └── views.py ├── manage.py ├── package-lock.json ├── package.json ├── public ├── index.html └── manifest.json ├── requirements.txt └── src ├── App.js ├── actions └── index.js ├── assets ├── fonts │ ├── avenir-next-medium.woff │ ├── avenir-next-regular.woff │ └── avenir-next-thin.woff ├── img │ ├── candidate.svg │ ├── coldoutreach-logo.png │ ├── decor.svg │ ├── email.svg │ ├── generate.svg │ ├── recruit_1.svg │ ├── recruit_2.svg │ ├── recruit_3.svg │ └── slack.svg └── styles │ └── scss │ ├── base.scss │ ├── card.scss │ ├── email.scss │ ├── header.scss │ ├── home.scss │ ├── info.scss │ ├── resume.scss │ └── testimonial.scss ├── components ├── Editor.js ├── Email.js ├── Header.js ├── Home.js ├── Resume.js └── landing │ ├── Card.js │ ├── Info.js │ ├── Slide.js │ └── Testimonial.js ├── containers ├── Email.js ├── Home.js ├── Resume.js └── index.js ├── epics ├── index.js └── resume.js ├── index.js ├── reducers ├── index.js └── resume.js ├── registerServiceWorker.js ├── routes.js └── store.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | # 4 space indentation 12 | [*.{html,scss}] 13 | indent_size = 4 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:jsx-a11y/recommended"], 3 | "plugins": ["jsx-a11y"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.pyc 3 | staticfiles 4 | .env 5 | .idea 6 | .vscode 7 | node_modules 8 | db.sqlite3 9 | *.css 10 | build/ 11 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate 2 | web: gunicorn coldoutreach.wsgi --log-file - -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coldoutreach 2 | [Coldoutreach Recruitment App](https://github.com/gitcoinco/skunkworks/issues/38) 3 | 4 | 5 | ### Objective 6 | To generate personalized cold-call emails based on the procedure outlined by Kevin Owocki in [this blog post](https://owocki.com/recruit-a-list-engineers-what-a-cold-recruitment-message-should-look-like/) 7 | 8 | ### Frontend 9 | ```sh 10 | $ npm i 11 | $ npm start 12 | ``` 13 | 14 | ### Backend 15 | ```sh 16 | $ pip3 install -r requirements.txt 17 | $ python3 manage.py runserver 18 | ``` 19 | 20 | ### Staging Site 21 | [Coldoutreach Staging](https://coldoutreach-staging.herokuapp.com) 22 | -------------------------------------------------------------------------------- /coldoutreach/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/coldoutreach/__init__.py -------------------------------------------------------------------------------- /coldoutreach/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for coldoutreach project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 18 | STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'r&5nft-mspqiseydm3-4+o9ifx=y-ji%a!vfy$ocdd2!$elc$$' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ["coldoutreach-staging.herokuapp.com", "127.0.0.1"] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'generator.apps.GeneratorConfig', 42 | 'corsheaders' 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'corsheaders.middleware.CorsMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'coldoutreach.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [ 62 | os.path.join(BASE_DIR, 'build') 63 | ], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'coldoutreach.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | STATICFILES_DIRS = [ 129 | os.path.join(BASE_DIR, 'build/static') 130 | ] -------------------------------------------------------------------------------- /coldoutreach/urls.py: -------------------------------------------------------------------------------- 1 | """coldoutreach URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include, re_path 17 | from django.contrib import admin 18 | from django.views.generic import TemplateView 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', admin.site.urls), 22 | url(r'api/', include('generator.urls')), 23 | re_path('.*', TemplateView.as_view(template_name='index.html')) 24 | ] 25 | -------------------------------------------------------------------------------- /coldoutreach/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for coldoutreach 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/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | from whitenoise.django import DjangoWhiteNoise 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "coldoutreach.settings") 16 | 17 | application = get_wsgi_application() 18 | application = DjangoWhiteNoise(application) -------------------------------------------------------------------------------- /generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/__init__.py -------------------------------------------------------------------------------- /generator/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | # Register your models here. 7 | -------------------------------------------------------------------------------- /generator/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class GeneratorConfig(AppConfig): 8 | name = 'generator' 9 | -------------------------------------------------------------------------------- /generator/fixtures/Christopher_Salat_Ceev.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/fixtures/Christopher_Salat_Ceev.pdf -------------------------------------------------------------------------------- /generator/fixtures/DanBernierProfile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/fixtures/DanBernierProfile.pdf -------------------------------------------------------------------------------- /generator/fixtures/DylanHirschkornProfile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/fixtures/DylanHirschkornProfile.pdf -------------------------------------------------------------------------------- /generator/fixtures/SeanDuganMurphyProfile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/fixtures/SeanDuganMurphyProfile.pdf -------------------------------------------------------------------------------- /generator/fixtures/TariqAliProfile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/fixtures/TariqAliProfile.pdf -------------------------------------------------------------------------------- /generator/fixtures/Tariq_Ali.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/fixtures/Tariq_Ali.pdf -------------------------------------------------------------------------------- /generator/letter_generator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import Counter 3 | import tracery 4 | from tracery.modifiers import base_english 5 | 6 | # To generate the letter, we use pytracery (https://github.com/aparrish/pytracery), a Python port of the Tracery text expansion library. 7 | def create_letter(keyword): 8 | rules = { 9 | 'origin': '#introduction# #talk_about_talent# #lead_into_job_description#', 10 | 'introduction': ['#found# your #website# via Google and #loved# #reading# it.', '#found# your #website#, and #loved# #reading# it.', '#found# your #website# via a keyword search.'], 11 | 'found': ['Found', 'Stumbled upon', 'Discovered'], 12 | 'loved': ['loved', 'enjoyed', 'appreciated'], 13 | 'website': ['website', 'blog', 'profile', 'resume'], 14 | 'reading': ['reading', 'looking at'], 15 | 'talk_about_talent': ['We share the same #passion# for KEYWORD.','You look like you #know# #keyword_praise#.'], 16 | 'know': ['know', 'understand'], 17 | 'keyword_praise': ['your KEYWORD-fu', 'KEYWORD deeply', 'KEYWORD down to a science'], 18 | 'passion': ['passion', 'love', 'affection', 'zeal', 'devotion'], 19 | 'lead_into_job_description': ['I have #interesting.a# project for you to look at.', "My company is looking for #interesting# people like you."], 20 | 'interesting': ['interesting', 'cool', 'awesome', 'neat'], 21 | } 22 | grammar = tracery.Grammar(rules) 23 | grammar.add_modifiers(base_english) 24 | letter = grammar.flatten("#origin#").replace("KEYWORD", keyword) 25 | return letter 26 | 27 | def convert_json_string_to_set(json_string): 28 | json_file = json.loads(json_string) 29 | counter = json_file['counter'] 30 | return Counter(counter) 31 | 32 | def generate(recruiter_json, candidate_json): 33 | recruiter_counter = convert_json_string_to_set(recruiter_json) 34 | candidate_counter = convert_json_string_to_set(candidate_json) 35 | combined_set = recruiter_counter & candidate_counter 36 | greatest_keyword = combined_set.most_common(1) 37 | if (len(greatest_keyword) > 0): 38 | greatest_keyword = greatest_keyword[0][0] 39 | else: 40 | greatest_keyword = "software" 41 | generated_letter = create_letter(greatest_keyword) 42 | return generated_letter -------------------------------------------------------------------------------- /generator/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-02-04 16:43 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='LinkedInPDF', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('guid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 20 | ('json', models.TextField()), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /generator/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/generator/migrations/__init__.py -------------------------------------------------------------------------------- /generator/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | import uuid 7 | 8 | # Create your models here. 9 | class LinkedInPDF(models.Model): 10 | guid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 11 | json = models.TextField() -------------------------------------------------------------------------------- /generator/resume_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # dependency - pdfminer 4 | # or pdfminer.six (for python3) 5 | 6 | # Originally from https://github.com/hellpanderrr/linkedin-pdf-parsing 7 | # But its codebase was changed heavily 8 | # License - GNU 2.0, will probably relicense this up... 9 | 10 | # -*- coding: utf8 -*- 11 | import os 12 | import re 13 | import sqlite3 14 | import argparse 15 | import sys 16 | import glob 17 | # from collections import OrderedDict 18 | from collections import Counter 19 | 20 | import generator.technology_jargon as technology_jargon 21 | 22 | from pdfminer.pdfparser import PDFParser 23 | from pdfminer.pdfdocument import PDFDocument 24 | from pdfminer.pdfpage import PDFPage 25 | from pdfminer.pdfpage import PDFTextExtractionNotAllowed 26 | from pdfminer.pdfinterp import PDFResourceManager 27 | from pdfminer.pdfinterp import PDFPageInterpreter 28 | from pdfminer.pdfdevice import PDFDevice 29 | from pdfminer.layout import LAParams 30 | from pdfminer.converter import PDFPageAggregator 31 | from pdfminer.layout import LAParams, LTTextBox, LTTextLine, LTFigure, LTImage, LTTextLineHorizontal, LTChar, LTLine, \ 32 | LTText 33 | import json 34 | 35 | import re 36 | 37 | isiterable = lambda obj: isinstance(obj, str) or getattr(obj, '__iter__', False) 38 | 39 | def get_objects(layout): 40 | # collecting all objects from the layout, 1 level depth 41 | objs = [] 42 | for obj in layout: 43 | 44 | if isiterable(obj): 45 | for element in obj: 46 | objs.append(element) 47 | else: 48 | objs.append(obj) 49 | return objs 50 | 51 | def convert_pdf_to_objects(input_file): 52 | f = input_file 53 | fp = open(f, 'rb') 54 | # Create a PDF parser object associated with the file object. 55 | parser = PDFParser(fp) 56 | # Create a PDF document object that stores the document structure. 57 | document = PDFDocument(parser) 58 | # Check if the document allows text extraction. If not, abort. 59 | if not document.is_extractable: 60 | raise PDFTextExtractionNotAllowed 61 | # Create a PDF resource manager object that stores shared resources. 62 | rsrcmgr = PDFResourceManager() 63 | # Create a PDF device object. 64 | laparams = LAParams() 65 | # Create a PDF page aggregator object. 66 | device = PDFPageAggregator(rsrcmgr, laparams=laparams) 67 | interpreter = PDFPageInterpreter(rsrcmgr, device) 68 | objs = [] 69 | for page in PDFPage.create_pages(document): 70 | interpreter.process_page(page) 71 | # receive the LTPage object for the page. 72 | layout = device.get_result() 73 | # collecting objects from the all pages, sorting them by their Y coordinate 74 | objs.append(sorted(get_objects(layout), key=lambda x: x.y0, reverse=True)) 75 | objs = sum(objs, []) # flattening to 1D array 76 | # getting objects from the corresponding sections 77 | return objs 78 | 79 | def extract_text(objs): 80 | text = "" 81 | for obj in objs: 82 | if "get_text" in dir(obj): 83 | text += obj.get_text() 84 | cleansed_text = text.replace('\n', ' ') 85 | return cleansed_text 86 | 87 | def count_of_technology_words(word, resume_as_text): 88 | escaped_word = re.escape(word) 89 | regex_pattern = r'(^|\s|[.]){word}($|\s|[.])'.format(word=escaped_word) 90 | array = re.findall(regex_pattern, resume_as_text, re.IGNORECASE) 91 | return len(array) 92 | 93 | def get_counter(resume_as_text): 94 | counter = Counter() 95 | 96 | for word in technology_jargon.keywords: 97 | count = count_of_technology_words(word, resume_as_text) 98 | if count > 0: 99 | counter[word] += count 100 | 101 | return counter 102 | 103 | def extract_data(resume_as_text, regex_pattern): 104 | match_object = re.search(regex_pattern, resume_as_text) 105 | if match_object: 106 | return match_object.group(0) 107 | else: 108 | return "" 109 | 110 | def get_name(resume_as_text): 111 | # Normally, a candidate's name has two words, both of which starts with capital letters 112 | normal_name_regex_pattern = r'([A-Z][a-z]*)(\s[A-Z][a-z]*)' 113 | normal_name = extract_data(resume_as_text, normal_name_regex_pattern) 114 | 115 | # One resume in the test set displays the name of the candidate in all-caps, which is really annoying. So we have this code to handle the edge case. 116 | edge_case_regex_pattern = r'([A-Za-z]*)(\s[A-Za-z]*)' 117 | edge_case_name = extract_data(resume_as_text, edge_case_regex_pattern) 118 | 119 | if len(edge_case_name) > len(normal_name): 120 | return edge_case_name.title() 121 | else: 122 | return normal_name.title() 123 | 124 | def get_email(resume_as_text): 125 | email_regex_pattern = r'([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})' 126 | email = extract_data(resume_as_text, email_regex_pattern) 127 | return email 128 | 129 | def convert(input_file): 130 | objs = convert_pdf_to_objects(input_file) 131 | 132 | resume_as_text = extract_text(objs) 133 | 134 | name = get_name(resume_as_text) 135 | email = get_email(resume_as_text) 136 | counter = get_counter(resume_as_text) 137 | 138 | dictionary = { "counter": counter, "name": name, "email": email} 139 | 140 | return json.dumps(dictionary) 141 | -------------------------------------------------------------------------------- /generator/resume_saver.py: -------------------------------------------------------------------------------- 1 | from django.core.files.storage import FileSystemStorage 2 | from generator.models import LinkedInPDF 3 | from collections import namedtuple 4 | import generator.resume_parser as resume_parser 5 | import uuid 6 | 7 | Resume = namedtuple('Resume', ['guid', 'json']) 8 | 9 | def save(myfile): 10 | fs = FileSystemStorage() 11 | extension = myfile.name.split('.')[-1] 12 | random_name = uuid.uuid4() 13 | filename ="%s.%s" % (random_name, extension) 14 | fs.save(filename, myfile) 15 | uploaded_file_json = resume_parser.convert(filename) 16 | fs.delete(filename) 17 | 18 | # While this works locally, the problem is that it does not work in Heroku. This is because of Heroku's file system, which does not play nice with SQLite. We'd need to switch over to Postgres. 19 | # pdf = LinkedInPDF(guid=random_name, json=uploaded_file_json) 20 | # pdf.save() 21 | 22 | return Resume(guid=random_name, json=uploaded_file_json) 23 | -------------------------------------------------------------------------------- /generator/technology_jargon.py: -------------------------------------------------------------------------------- 1 | array = [ 2 | # https://github.com/dariusk/corpora/blob/master/data/technology/programming_languages.json 3 | # Accessed Feb. 8, 2018 4 | "ABAP", 5 | "ActionScript", 6 | "Ada", 7 | "Assembly language", 8 | "AutoHotkey", 9 | "AutoIt", 10 | "Awk", 11 | "C", 12 | "C#", 13 | "C++", 14 | "COBOL", 15 | "CSS", 16 | "CoffeeScript", 17 | "D", 18 | "Dart", 19 | "Erlang", 20 | "F#", 21 | "Fortran", 22 | "FoxPro", 23 | "Go", 24 | "Groovy", 25 | "Haskell", 26 | "Java", 27 | "JavaScript", 28 | "LabVIEW", 29 | "Ladder Logic", 30 | "Lisp", 31 | "Logo", 32 | "Lua", 33 | "MATLAB", 34 | "ML", 35 | "Object Pascal", 36 | "Objective-C", 37 | "OpenEdge ABL", 38 | "PHP", 39 | "PL/SQL", 40 | "Pascal", 41 | "Perl", 42 | "Prolog", 43 | "Python", 44 | "R", 45 | "RPG (OS/400)", 46 | "Ruby", 47 | "Rust", 48 | "SAS", 49 | "Swift", 50 | "TeX", 51 | "Transact-SQL", 52 | "VBScript", 53 | "VHDL", 54 | "Visual Basic", 55 | # https://github.com/dariusk/corpora/blob/master/data/technology/computer_sciences.json 56 | # accessed Feb. 8, 2018 57 | ".QL", 58 | "ActionScript", 59 | "ActiveRecord", 60 | "AIM", 61 | "Ajax", 62 | "Algol", 63 | "Amazon", 64 | "Angular", 65 | "Apache", 66 | "AppleScript", 67 | "AppStream", 68 | "ASPnet", 69 | "AutoHotKey", 70 | "Aviato", 71 | "AWS", 72 | "Backbone", 73 | "BASIC", 74 | "Bootstrap", 75 | "Bower", 76 | "Browserify", 77 | "Bundler", 78 | "Csharp", 79 | "Canvas", 80 | "Capistrano", 81 | "Cassandra", 82 | "ClearDB", 83 | "CloudFormation", 84 | "CloudFront", 85 | "CloudSearch", 86 | "CloudTrail", 87 | "CloudWatch", 88 | "CodeCommit", 89 | "CodeDeploy", 90 | "CodePipeline", 91 | "CoffeeScript", 92 | "Cognito", 93 | "CouchDB", 94 | "CrunchBang", 95 | "CSS3", 96 | "Cucumber", 97 | "D3", 98 | "Dart", 99 | "Diaspora", 100 | "Discourse", 101 | "Django", 102 | "Drupal", 103 | "DynamoDB", 104 | "EBS", 105 | "EC2", 106 | "EJS", 107 | "ElacticBeanstalk", 108 | "Elasticache", 109 | "ElasticSearch", 110 | "Emacs", 111 | "Ember", 112 | "ERB", 113 | "Erlang", 114 | "Express", 115 | "Facebook", 116 | "Fedora", 117 | "Foursquare", 118 | "Flash", 119 | "Flickr", 120 | "FORTRAN", 121 | "Foundation", 122 | "FTP", 123 | "Ghost", 124 | "GitHub", 125 | "Glacier", 126 | "Gmail", 127 | "GNUemacs", 128 | "GNUlinux", 129 | "GoogleDocs", 130 | "GoogleMaps", 131 | "GrapheneDB", 132 | "Grunt", 133 | "HacketyHack", 134 | "Hadoop", 135 | "Heroku", 136 | "Hipchat", 137 | "HTML5", 138 | "ICQ", 139 | "IFTT", 140 | "ImageMagick", 141 | "Imgur", 142 | "Indiegogo", 143 | "Instagram", 144 | "IRB", 145 | "IRC", 146 | "IronCache", 147 | "Jasmine", 148 | "Java", 149 | "Javascript", 150 | "Jekyll", 151 | "jQuery", 152 | "KeenIO", 153 | "Kickstarter", 154 | "Knockout", 155 | "LaTeX", 156 | "LeapMotion", 157 | "LevelDB", 158 | "Linux", 159 | "Lisp", 160 | "Lyft", 161 | "MariaDB", 162 | "MarkDown", 163 | "Memcached", 164 | "Middleman", 165 | "Minitest", 166 | "Mocha", 167 | "MongoDB", 168 | "MySql", 169 | "Netflix", 170 | "NewRelic", 171 | "Nginx", 172 | "NLTK", 173 | "Node.js", 174 | "Nokogiri", 175 | "NoSQL", 176 | "NPM", 177 | "OAuth", 178 | "Objective-C", 179 | "OCaml", 180 | "OCR", 181 | "Octopress", 182 | "OculusRift", 183 | "OpenCV", 184 | "Opera", 185 | "Oracle", 186 | "Pandora", 187 | "Passenger", 188 | "Perl", 189 | "PGP", 190 | "PHP", 191 | "PIP", 192 | "Polymer", 193 | "Postgres", 194 | "Processing", 195 | "PubNub", 196 | "PushNotifications", 197 | "Python", 198 | "Quora", 199 | "Rack", 200 | "Rails", 201 | "React", 202 | "RedHat", 203 | "Redis", 204 | "Refinery", 205 | "Route53", 206 | "RSpec", 207 | "Ruby", 208 | "Rust", 209 | "Sails", 210 | "Scala", 211 | "Scheme", 212 | "Scratch", 213 | "Sendgrid", 214 | "SES", 215 | "Silverlight", 216 | "Sinatra", 217 | "Slack", 218 | "SNS", 219 | "Solr", 220 | "Spotify", 221 | "SpoonRocket", 222 | "Sqlite", 223 | "SQS", 224 | "SSH", 225 | "Swift", 226 | "SWF", 227 | "TCP", 228 | "TempoDB", 229 | "Tumblr", 230 | "Twilio", 231 | "Twitter", 232 | "Uber", 233 | "Ubuntu", 234 | "UbuWeb", 235 | "Unicorn", 236 | "VBScript", 237 | "Vim", 238 | "VisualBasic", 239 | "Webaudio", 240 | "Webrick", 241 | "Websockets", 242 | "Wolfram Language", 243 | "WordPress", 244 | "XTags", 245 | "Yahoo", 246 | "Yelp", 247 | "YouTube", 248 | "Zepto", 249 | # https://github.com/dariusk/corpora/blob/master/data/technology/lisp.json 250 | "ABCL", 251 | "ACL", 252 | "ACL2", 253 | "ANSI Common Lisp", 254 | "Allegro CL", 255 | "Arc", 256 | "Armed Bear CL", 257 | "AutoLISP", 258 | "CCL", 259 | "CLISP", 260 | "CLiCC", 261 | "CMU CL", 262 | "CMUCL", 263 | "Clasp", 264 | "Clojure", 265 | "Clozure CL", 266 | "Common Lisp", 267 | "Corman CL", 268 | "Dylan", 269 | "ECL", 270 | "emacs-cl", 271 | "Emacs Lisp", 272 | "Embedded CL", 273 | "EuLisp", 274 | "Franz Lisp", 275 | "GCL", 276 | "GNU CL", 277 | "GNU CLISP", 278 | "Hy", 279 | "IEEE Scheme", 280 | "ISLISP", 281 | "InterLisp", 282 | "LFE", 283 | "LISP 1 ", 284 | "LISP 1.5", 285 | "LeLisp", 286 | "Lisp Machine Lisp", 287 | "LispWorks", 288 | "MACLISP", 289 | "MDL", 290 | "MKCL", 291 | "Maclisp", 292 | "Movitz", 293 | "NIL", 294 | "Newlisp", 295 | "Picolisp", 296 | "Poplog", 297 | "Portable Standard Lisp", 298 | "RPL", 299 | "Racket", 300 | "SBCL", 301 | "SKILL", 302 | "Scheme", 303 | "Scieneer CL", 304 | "Spice Lisp", 305 | "Standard Lisp ", 306 | "Stanford LISP 1.6", 307 | "Steel Bank CL", 308 | "T", 309 | "ThinLisp", 310 | "UABCL", 311 | "WCL", 312 | "XCL", 313 | "XLISP", 314 | "ZetaLisp", 315 | # Additional words not in the corpus but are technology related 316 | ".NET", 317 | "blockchain", 318 | "Ethereum", 319 | "Bitcoin", 320 | ] 321 | 322 | keywords = set(word for word in array) -------------------------------------------------------------------------------- /generator/templates/generator/email.html: -------------------------------------------------------------------------------- 1 | Recruiter's LinkedIn Profile: - "https://www.linkedin.com/in/trali" 2 | Candidate's LinkedIn Profile: 3 | 4 | 7 | 8 | {% load static %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /generator/templates/generator/index.html: -------------------------------------------------------------------------------- 1 |

A Better Way To Find Talent

2 | 3 | Instantly create custom outreach emails tailored to each candidate. 4 | 5 |
6 | {% csrf_token %} 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | {% if uploaded_file_json %} 15 |

Here is the parsed JSON of the recruiter PDF:

16 | 17 | {{ uploaded_file_json }} 18 | 19 | 20 |

Here is the parsed JSON of the candidate PDF:

21 | 22 | {{ uploaded_file_json_2 }} 23 | 24 | 25 |

And here's an example genereated letter:

26 | 27 | {{ generated_text }} 28 | 29 | {% endif %} 30 | 31 | Connect with Linkedin -------------------------------------------------------------------------------- /generator/templates/generator/profile.html: -------------------------------------------------------------------------------- 1 | YOU AUTHENTICATED! This is just the landing page that shows up when you are logged into our system... 2 | 3 | After you select a profile to market to, just click this link to visit the email page... -------------------------------------------------------------------------------- /generator/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | from collections import Counter 7 | 8 | import generator.resume_parser as resume_parser 9 | import os 10 | import json 11 | 12 | class TestResumeParser(TestCase): 13 | def load_resume(self, resume_name): 14 | path_to_directory = "generator/fixtures/{resume_name}.pdf".format(resume_name=resume_name) 15 | file_path = os.path.abspath(path_to_directory) 16 | json_string = resume_parser.convert(file_path) 17 | json_file = json.loads(json_string) 18 | return json_file 19 | 20 | def convert_to_counter(self, json_file): 21 | counter = json_file["counter"] 22 | return Counter(counter) 23 | 24 | def generate_counter(self, resume_name): 25 | json_file = self.load_resume(resume_name) 26 | return self.convert_to_counter(json_file) 27 | 28 | def generate_name(self, resume_name): 29 | json_file = self.load_resume(resume_name) 30 | return json_file["name"] 31 | 32 | def generate_email(self, resume_name): 33 | json_file = self.load_resume(resume_name) 34 | return json_file["email"] 35 | 36 | def test_parse_tariq_ali_profile_counter(self): 37 | expected_counter = Counter({'Ruby': 8, 'Rails': 5, 'WordPress': 3, 'Bootstrap': 2, 'JavaScript': 1, 'jQuery': 1, '.NET': 1, 'C#': 1, 'RSpec': 1, 'Sinatra': 1, 'C++': 1, 'Angular': 1, 'Javascript': 1, 'Ethereum': 1, 'blockchain': 1}) 38 | actual_counter = self.generate_counter("TariqAliProfile") 39 | self.assertEqual(expected_counter, actual_counter) 40 | 41 | def test_parse_tariq_ali_profile_name(self): 42 | expected_name = "Tariq Ali" 43 | actual_name = self.generate_name("TariqAliProfile") 44 | self.assertEqual(expected_name, actual_name) 45 | 46 | def test_parse_tariq_ali_profile_email(self): 47 | expected_email = "tra38@nau.edu" 48 | actual_email = self.generate_email("TariqAliProfile") 49 | self.assertEqual(expected_email, actual_email) 50 | 51 | def test_parse_second_tariq_ali_profile_counter(self): 52 | expected_counter = Counter({'Ruby': 15, 'Rails': 5, 'WordPress': 3, 'Angular': 3, 'Sinatra': 2, 'jQuery': 2, 'JavaScript': 2, 'C++': 2, 'Twitter': 2, 'Javascript': 2, 'Bootstrap': 2, 'GitHub': 1, '.NET': 1, 'RSpec': 1, 'blockchain': 1, 'Ethereum': 1, 'Capistrano': 1, 'AWS': 1, 'C#': 1, 'React': 1}) 53 | actual_counter = self.generate_counter("Tariq_Ali") 54 | self.assertEqual(expected_counter, actual_counter) 55 | 56 | def test_parse_second_tariq_ali_profile_name(self): 57 | expected_name = "Tariq\xa0Ali" 58 | actual_name = self.generate_name("Tariq_Ali") 59 | self.assertEqual(expected_name, actual_name) 60 | 61 | def test_parse_second_tariq_ali_profile_email(self): 62 | expected_email = "tra38@nau.edu" 63 | actual_email = self.generate_email("Tariq_Ali") 64 | self.assertEqual(expected_email, actual_email) 65 | 66 | def test_parse_dan_bernier_profile_counter(self): 67 | expected_counter = Counter({'Ruby': 7, 'Processing': 4, 'C#': 3, 'Rails': 2, 'Javascript': 1, '.NET': 1, 'JavaScript': 1, 'Scheme': 1}) 68 | actual_counter = self.generate_counter("DanBernierProfile") 69 | self.assertEqual(expected_counter, actual_counter) 70 | 71 | def test_parse_dan_bernier_profile_name(self): 72 | expected_name = "Dan Bernier" 73 | actual_name = self.generate_name("DanBernierProfile") 74 | self.assertEqual(expected_name, actual_name) 75 | 76 | def test_parse_dan_bernier_profile_email(self): 77 | expected_email = "danbernier@gmail.com" 78 | actual_email = self.generate_email("DanBernierProfile") 79 | self.assertEqual(expected_email, actual_email) 80 | 81 | def test_parse_dylan_hirschkorn_profile_counter(self): 82 | expected_counter = Counter({'Dylan': 3, 'Visual Basic': 3, 'BASIC': 3, 'C#': 2, 'Swift': 1}) 83 | # This is a bug, Dylan only mentioned "Visual Basic", not "Basic" on his resume. However, I do not know of a good way of fixing this specific edge case. Also, Dylan is the name of a programming language, which is why Dylan shows up in the counter. 84 | actual_counter = self.generate_counter("DylanHirschkornProfile") 85 | self.assertEqual(expected_counter, actual_counter) 86 | 87 | def test_parse_dylan_hirschkorn_profile_name(self): 88 | expected_name = "Dylan Hirschkorn" 89 | actual_name = self.generate_name("DylanHirschkornProfile") 90 | self.assertEqual(expected_name, actual_name) 91 | 92 | def test_parse_dylan_hirschkorn_profile_email(self): 93 | expected_email = "" 94 | actual_email = self.generate_email("DylanHirschkornProfile") 95 | self.assertEqual(expected_email, actual_email) 96 | 97 | def test_parse_sean_dugan_murphy_profile_counter(self): 98 | expected_counter = Counter({'Swift': 11, 'Twitter': 3, 'Objective-C': 3, 'Facebook': 3, 'GitHub': 2, 'YouTube': 2, 'CSS': 1, 'C#': 1}) 99 | actual_counter = self.generate_counter("SeanDuganMurphyProfile") 100 | self.assertEqual(expected_counter, actual_counter) 101 | 102 | def test_parse_sean_dugan_murphy_profile_name(self): 103 | # The full name of the candidate is Sean Dugan Murphy. However we assume that a candidate only has a first and last name...and ignore the edge case where a candidate has a middle name. 104 | expected_name = "Sean Dugan" 105 | actual_name = self.generate_name("SeanDuganMurphyProfile") 106 | self.assertEqual(expected_name, actual_name) 107 | 108 | def test_parse_sean_dugan_murphy_profile_email(self): 109 | expected_email = "" 110 | actual_email = self.generate_email("SeanDuganMurphyProfile") 111 | self.assertEqual(expected_email, actual_email) 112 | 113 | def test_parse_christopher_salat_ceev_counter(self): 114 | # Note that Christopher Salat does not actually know either PHP or Scratch. He links to several websites that end with the .php extension and he serves as a Scratch DJ. This indicates a problem with relying solely on keywords detached from the context. 115 | expected_counter = Counter({'YouTube': 5, 'PHP': 2, 'Scratch': 1}) 116 | actual_counter = self.generate_counter("Christopher_Salat_Ceev") 117 | self.assertEqual(expected_counter, actual_counter) 118 | 119 | def test_parse_christopher_salat_ceev_name(self): 120 | expected_name = "Christopher Salat" 121 | actual_name = self.generate_name("Christopher_Salat_Ceev") 122 | self.assertEqual(expected_name, actual_name) 123 | 124 | def test_parse_christopher_salat_ceev_email(self): 125 | expected_email = "christopherzerker@gmail.com" 126 | actual_email = self.generate_email("Christopher_Salat_Ceev") 127 | self.assertEqual(expected_email, actual_email) 128 | -------------------------------------------------------------------------------- /generator/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | path('profile', views.profile, name='profile'), 8 | path('email', views.email, name='email'), 9 | ] -------------------------------------------------------------------------------- /generator/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import render 5 | 6 | import generator.resume_saver as resume_saver 7 | 8 | import generator.letter_generator as letter_generator 9 | 10 | from django.http import JsonResponse 11 | 12 | from django.views.decorators.csrf import csrf_exempt 13 | 14 | import json 15 | 16 | def index(request): 17 | context = {} 18 | if request.method == 'POST': 19 | if request.FILES['recruiter_file']: 20 | recruiter_file = request.FILES['recruiter_file'] 21 | json_blob_recruiter = resume_saver.save(recruiter_file) 22 | context["uploaded_file_json"] = json_blob_recruiter.json 23 | if request.FILES['candidate_file']: 24 | candidate_file = request.FILES['candidate_file'] 25 | json_blob_candidate = resume_saver.save(candidate_file) 26 | context["uploaded_file_json_2"] = json_blob_candidate.json 27 | if context["uploaded_file_json"] and context["uploaded_file_json_2"]: 28 | context["generated_text"] = letter_generator.generate(context["uploaded_file_json"], context["uploaded_file_json_2"]) 29 | return render(request, 'generator/index.html', context) 30 | 31 | @csrf_exempt 32 | def profile(request): 33 | context = {} 34 | if request.FILES['recruiter_file']: 35 | recruiter_file = request.FILES['recruiter_file'] 36 | json_blob_recruiter = resume_saver.save(recruiter_file) 37 | context["recruiter_json"] = json_blob_recruiter.json 38 | if request.FILES['candidate_file']: 39 | candidate_file = request.FILES['candidate_file'] 40 | json_blob_candidate = resume_saver.save(candidate_file) 41 | context["candidate_json"] = json_blob_candidate.json 42 | if context["recruiter_json"] and context["candidate_json"]: 43 | context["generated_text"] = letter_generator.generate(context["recruiter_json"], context["candidate_json"]) 44 | return JsonResponse(context) 45 | 46 | @csrf_exempt 47 | def email(request): 48 | context = {} 49 | data = json.loads(request.body) 50 | if data['recruiter_json']: 51 | context["recruiter_json"] = data['recruiter_json'] 52 | if data['candidate_json']: 53 | context["candidate_json"] = data['candidate_json'] 54 | if context["recruiter_json"] and context["candidate_json"]: 55 | context["generated_text"] = letter_generator.generate(context["recruiter_json"], context["candidate_json"]) 56 | return JsonResponse(context) 57 | -------------------------------------------------------------------------------- /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", "coldoutreach.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coldoutreach", 3 | "version": "0.1.0", 4 | "description": "", 5 | "homepage": ".", 6 | "license": "ISC", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/gitcoinco/coldoutreach.git" 10 | }, 11 | "dependencies": { 12 | "antd": "^3.2.3", 13 | "dotenv": "^5.0.1", 14 | "history": "^4.7.2", 15 | "node-sass-chokidar": "1.1.0", 16 | "prettier": "^1.11.1", 17 | "react": "^16.2.0", 18 | "react-copy-to-clipboard": "^5.0.1", 19 | "react-dom": "^16.2.0", 20 | "react-lz-editor": "^0.11.9", 21 | "react-redux": "^5.0.7", 22 | "react-router": "^4.2.0", 23 | "react-router-dom": "^4.2.2", 24 | "react-router-redux": "^5.0.0-alpha.9", 25 | "react-scripts": "^1.1.1", 26 | "redux": "^3.7.2", 27 | "redux-actions": "^2.3.0", 28 | "redux-logger": "^3.0.6", 29 | "redux-observable": "^0.18.0", 30 | "rxjs": "^5.5.6" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "^8.2.2", 34 | "concurrently": "^3.5.1", 35 | "eslint": "^4.18.2", 36 | "eslint-config-react-app": "^2.1.0", 37 | "eslint-plugin-flowtype": "^2.46.1", 38 | "eslint-plugin-import": "^2.9.0", 39 | "eslint-plugin-jsx-a11y": "^6.0.3", 40 | "eslint-plugin-react": "^7.7.0", 41 | "mocha": "^5.0.4" 42 | }, 43 | "lint-staged": { 44 | "src/**/*.{js,jsx,json,css}": [ 45 | "prettier --single-quote --write", 46 | "git add" 47 | ] 48 | }, 49 | "proxy": { 50 | "/api": { 51 | "changeOrigin": true, 52 | "target": "http://127.0.0.1:8000/" 53 | } 54 | }, 55 | "scripts": { 56 | "build-css": "node-sass-chokidar src/assets/styles/scss -o src/assets/styles/css", 57 | "watch-css": "npm run build-css && node-sass-chokidar src/assets/styles/scss/ -o src/assets/styles/css/ --watch --recursive", 58 | "start": "concurrently --kill-others \"react-scripts start\" \"npm run watch-css\"", 59 | "build": "react-scripts build", 60 | "test": "react-scripts test --env=jsdom", 61 | "eject": "react-scripts eject", 62 | "postinstall": "npm run build-css && npm run build" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home | Coldoutreach 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | chardet==3.0.4 2 | Django==2.1.11 3 | django-cors-headers==2.1.0 4 | gunicorn==19.7.1 5 | pdfminer.six==20170720 6 | pycryptodome==3.6.6 7 | pytz==2017.3 8 | six==1.11.0 9 | tracery==0.1.1 10 | whitenoise==3.3.1 11 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | import './assets/styles/css/base.css'; 5 | 6 | // Routes 7 | import { routes } from './routes'; 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 |
13 | { routes.map((route) => ( 14 | 15 | ) ) } 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | 4 | export const UPLOAD_RESUME = 'app/resume/upload'; 5 | export const UPLOAD_RESUME_SUCCESS = 'app/resume/upload/success'; 6 | 7 | export const GENERATE_EMAIL = 'app/generate/email'; 8 | export const GENERATE_EMAIL_SUCCESS = 'app/generate/email/success'; 9 | 10 | export const ERROR = 'app/error'; 11 | 12 | 13 | export const UploadResume = createAction(UPLOAD_RESUME, (data) => (data)); 14 | export const UploadResumeSuccess = createAction(UPLOAD_RESUME_SUCCESS, (data) => (data)); 15 | 16 | export const GenerateEmail = createAction(GENERATE_EMAIL, (data) => (data)); 17 | export const GenerateEmailSuccess = createAction(GENERATE_EMAIL_SUCCESS, (data) => (data)); 18 | 19 | export const Error = createAction(ERROR, (error) => (error)); 20 | -------------------------------------------------------------------------------- /src/assets/fonts/avenir-next-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/src/assets/fonts/avenir-next-medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/avenir-next-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/src/assets/fonts/avenir-next-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/avenir-next-thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/src/assets/fonts/avenir-next-thin.woff -------------------------------------------------------------------------------- /src/assets/img/candidate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Sketch. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/img/coldoutreach-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/coldoutreach/7bf97c803ec6065493e33b6740aa690569bb1e14/src/assets/img/coldoutreach-logo.png -------------------------------------------------------------------------------- /src/assets/img/decor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/assets/img/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/img/generate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Sketch. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/img/recruit_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/img/recruit_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/img/recruit_3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Sketch. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/img/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created with Sketch. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/assets/styles/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | @font-face { 4 | font-family: "avenirNextThin"; 5 | src: url("../../fonts/avenir-next-thin.woff"); 6 | } 7 | 8 | @font-face { 9 | font-family: "avenirNext"; 10 | src: url("../../fonts/avenir-next-regular.woff"); 11 | } 12 | 13 | @font-face { 14 | font-family: "avenirNextMedium"; 15 | src: url("../../fonts/avenir-next-medium.woff"); 16 | } 17 | 18 | html { 19 | height: auto; 20 | min-height: 100% !important; 21 | } 22 | 23 | body { 24 | font-family: "avenirNext"; 25 | background-image: linear-gradient(134deg, #3023AE 0%, #C86DD7 100%); 26 | background-repeat: no-repeat; 27 | background-size: cover; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | 32 | h1, h2 { 33 | font-family: "avenirNextMedium"; 34 | } 35 | 36 | p { 37 | font-family: "avenirNextThin"; 38 | } 39 | 40 | @media (max-width: 650px) { 41 | html { 42 | height: auto; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/assets/styles/scss/card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | border-radius: 2px; 3 | height: 360px; 4 | width: 200px; 5 | text-align: center; 6 | background: #4e4376; 7 | 8 | img { 9 | margin-top: 4em; 10 | margin-bottom: 5em; 11 | height: 7em; 12 | } 13 | 14 | h2 { 15 | font-family: "avenirNext"; 16 | font-weight: bolder; 17 | color: #fff; 18 | line-height: 38px; 19 | letter-spacing: 1.15px; 20 | font-size: 1.6em; 21 | text-align: center; 22 | 23 | span { 24 | display: block; 25 | } 26 | } 27 | } 28 | 29 | @media (max-width: 576px) { 30 | .card { 31 | height: 200px; 32 | width: 100px; 33 | 34 | img { 35 | margin-bottom: 3em; 36 | } 37 | } 38 | } 39 | 40 | @media (max-width: 768px) { 41 | .card { 42 | margin-top: 4em; 43 | height: 300px; 44 | width: 200px; 45 | 46 | img { 47 | margin-bottom: 3.5em; 48 | height: 5em; 49 | } 50 | 51 | h2 { 52 | font-size: 1.3em; 53 | line-height: 30px; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/assets/styles/scss/email.scss: -------------------------------------------------------------------------------- 1 | .email { 2 | 3 | padding: 3em 0; 4 | 5 | .text { 6 | font-size: 12px; 7 | } 8 | 9 | .applicant-info { 10 | margin-bottom: 2em; 11 | } 12 | 13 | img { 14 | width: 6.5em; 15 | border-radius: 100%; 16 | } 17 | 18 | p, h2 { 19 | color: white; 20 | } 21 | 22 | p { 23 | margin-bottom: 5px; 24 | } 25 | 26 | h2 { 27 | font-size: 25px; 28 | margin-bottom: 0; 29 | } 30 | 31 | .designation { 32 | font-size: 14px; 33 | margin-bottom: 8px; 34 | border-radius: 3px; 35 | } 36 | 37 | .profile { 38 | font-size: 12px; 39 | color: black; 40 | background: white; 41 | padding: 5px; 42 | font-family: "avenirNext"; 43 | } 44 | 45 | .editor { 46 | background: #4e4376; 47 | padding: 10px 10px 15px; 48 | 49 | button { 50 | background-color: #595c7c; 51 | border-color: #595c7c; 52 | color: #FFFFFF; 53 | margin-top: 10px; 54 | margin-left: 8px; 55 | margin-right: 8px; 56 | } 57 | 58 | button:hover { 59 | background-color: #7a7c96; 60 | border-color: #7a7c96; 61 | } 62 | 63 | .generate { 64 | float: right; 65 | background-color: #3DD1AF; 66 | border-color: #3DD1AF; 67 | } 68 | 69 | .generate:hover { 70 | background-color: #52d6b8; 71 | border-color: #52d6b8; 72 | } 73 | 74 | .generate:focus { 75 | border-color: #36bc9d; 76 | background-color: #36bc9d; 77 | } 78 | } 79 | 80 | } 81 | 82 | @media (max-width: 576px) { 83 | .recruit-img { 84 | margin-top: 1em; 85 | } 86 | } 87 | 88 | @media (max-width: 768px) { 89 | .email { 90 | .editor .generate { 91 | float: none; 92 | } 93 | 94 | .recruit-img { 95 | text-align: center; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/assets/styles/scss/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | padding-top: 63px; 3 | 4 | img { 5 | width: 212px; 6 | height: 24px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/styles/scss/home.scss: -------------------------------------------------------------------------------- 1 | .landing-header { 2 | padding-top: 80px; 3 | padding-bottom: 125px; 4 | 5 | h1, h2, p, i { 6 | color: #FFFFFF; 7 | } 8 | 9 | .title { 10 | margin-top: 40px; 11 | font-size: 3em; 12 | letter-spacing: -0.5px; 13 | line-height: 72px; 14 | } 15 | 16 | .content { 17 | font-size: 1.5em; 18 | letter-spacing: -0.2px; 19 | 20 | span { 21 | display: block; 22 | } 23 | } 24 | 25 | a { 26 | border-color: #3DD1AF; 27 | background-color: #3DD1AF; 28 | padding-left: 0; 29 | padding-top: 7px; 30 | width: 220px; 31 | height: 42px; 32 | 33 | i { 34 | margin-top: 3px; 35 | margin-right: 10px; 36 | font-size: 20px; 37 | } 38 | 39 | span { 40 | border-left: 1px solid #F2F2F2; 41 | position: relative; 42 | padding-left: 10px; 43 | top: -2px; 44 | } 45 | } 46 | 47 | a:hover { 48 | border-color: #52d6b8; 49 | background-color: #52d6b8; 50 | } 51 | 52 | a:focus { 53 | border-color: #36bc9d; 54 | background-color: #36bc9d; 55 | } 56 | } 57 | 58 | .idea { 59 | padding-top: 50px; 60 | padding-bottom: 50px; 61 | border-top: 5px solid #50E3C2; 62 | background-color: #F4F4F4; 63 | 64 | h2 { 65 | font-weight: bold; 66 | margin-bottom: 25px; 67 | font-size: 1.75em; 68 | color: #525366; 69 | letter-spacing: -0.28px; 70 | line-height: 58px; 71 | } 72 | } 73 | 74 | .right-decor, .left-decor { 75 | position: absolute; 76 | } 77 | 78 | .right-decor { 79 | top: -64px; 80 | right: 60px; 81 | transform: rotate(90deg); 82 | } 83 | 84 | .left-decor { 85 | left: 0; 86 | bottom: 30px; 87 | } 88 | 89 | #recruit_3 img { 90 | position: relative; 91 | bottom: 8px; 92 | width: 53px; 93 | } 94 | 95 | .down { 96 | background: #FFFFFF; 97 | position: relative; 98 | text-align: center; 99 | font-weight: bold; 100 | 101 | button { 102 | bottom: 18px; 103 | 104 | i { 105 | position: relative; 106 | top: 2px; 107 | } 108 | } 109 | } 110 | 111 | @media (max-width: 576px) { 112 | .landing-header { 113 | .title { 114 | line-height: 40px; 115 | font-size: 2em; 116 | } 117 | 118 | .content { 119 | font-size: 1.3em; 120 | } 121 | 122 | .left { 123 | margin-bottom: 2em; 124 | } 125 | } 126 | } 127 | 128 | @media (max-width: 768px) { 129 | .right-decor, .left-decor { 130 | display: none; 131 | } 132 | 133 | .landing-header { 134 | .title { 135 | margin-top: 0; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/assets/styles/scss/info.scss: -------------------------------------------------------------------------------- 1 | .info { 2 | img { 3 | width: 50px; 4 | } 5 | 6 | h3 { 7 | color: #43484D; 8 | font-size: 20px; 9 | letter-spacing: 0; 10 | line-height: 25px; 11 | } 12 | 13 | p { 14 | font-size: 16px; 15 | color: #86939E; 16 | letter-spacing: 0; 17 | line-height: 25px; 18 | } 19 | } 20 | 21 | @media (max-width: 576px) { 22 | .info { 23 | h3 { 24 | font-size: 18px; 25 | } 26 | 27 | p { 28 | font-size: 14px; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/styles/scss/resume.scss: -------------------------------------------------------------------------------- 1 | .gradient { 2 | height: 100%; 3 | } 4 | 5 | .resume { 6 | padding-top: 80px; 7 | padding-bottom: 40px; 8 | 9 | h2, h3, h4, ul, button { 10 | color: #FFFFFF; 11 | } 12 | } 13 | 14 | .upload-box { 15 | margin-top: 3em; 16 | padding: 2em 4em 4em; 17 | text-align: center; 18 | background: #4e4376; 19 | border-radius: 2px; 20 | 21 | h4 { 22 | text-align: center; 23 | font-size: 18px; 24 | letter-spacing: 0; 25 | line-height: 32px; 26 | } 27 | 28 | .ant-upload { 29 | width: 128px; 30 | height: 128px; 31 | border-radius: 4px; 32 | background-color: #fafafa; 33 | text-align: center; 34 | cursor: pointer; 35 | -webkit-transition: border-color 0.3s ease; 36 | transition: border-color 0.3s ease; 37 | vertical-align: top; 38 | display: table; 39 | margin: 0 auto; 40 | 41 | span { 42 | width: 100%; 43 | height: 100%; 44 | display: table-cell; 45 | text-align: center; 46 | vertical-align: middle; 47 | padding: 8px; 48 | } 49 | 50 | i { 51 | font-size: 32px; 52 | color: #999; 53 | } 54 | } 55 | 56 | .ant-upload-list-item { 57 | background: #fff; 58 | border-radius: 2px; 59 | width: 90%; 60 | margin: 0 auto; 61 | } 62 | 63 | .ant-btn { 64 | font-family: "avenirNext"; 65 | font-weight: "bolder"; 66 | font-size: 15px; 67 | background: #3DD1AF; 68 | border-radius: 3px; 69 | margin-top: 40px; 70 | margin-left: 10px; 71 | border-color: #3DD1AF; 72 | min-width: 175px; 73 | height: 50px; 74 | 75 | &:hover { 76 | color: #FFFFFF; 77 | background-color: #52d6b8; 78 | border-color: #52d6b8; 79 | } 80 | 81 | &:focus { 82 | color: #FFFFFF; 83 | border-color: #36bc9d; 84 | background-color: #36bc9d; 85 | } 86 | } 87 | } 88 | 89 | .upload-box::after { 90 | content: ""; 91 | top: 0; 92 | left: 0; 93 | position: relative; 94 | background: #42445C; 95 | opacity: 0.74; 96 | z-index: -1; 97 | } 98 | 99 | //@media (max-width: 768px) { } 100 | 101 | @media (max-width: 650px) { 102 | .upload-box { 103 | padding-top: 40px; 104 | padding-left: 2em; 105 | padding-right: 2em; 106 | 107 | h4 { 108 | font-size: 15px; 109 | } 110 | 111 | .ant-btn { 112 | font-size: 13px; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/assets/styles/scss/testimonial.scss: -------------------------------------------------------------------------------- 1 | .testimonials { 2 | background: #FFF; 3 | padding-top: 30px; 4 | padding-bottom: 30px; 5 | 6 | .ant-carousel { 7 | .slick-slide { 8 | height: 220px; 9 | line-height: 160px; 10 | background: #FFF; 11 | overflow: hidden; 12 | } 13 | 14 | .slick-dots li button { 15 | background-color: black; 16 | } 17 | } 18 | 19 | .photo { 20 | img { 21 | float: right; 22 | margin-top: 15px; 23 | width: 120px; 24 | height: 120px; 25 | border-radius: 4px; 26 | } 27 | } 28 | 29 | #org { 30 | top: -4px; 31 | right: -12px; 32 | position: absolute; 33 | width: 40px; 34 | height: 40px; 35 | z-index: 2; 36 | margin: 0; 37 | } 38 | 39 | .quote { 40 | height: 120px; 41 | font-family: "avenirNext"; 42 | font-size: 22px; 43 | color: #43484C; 44 | letter-spacing: 0; 45 | line-height: 46px; 46 | margin-bottom: 1em; 47 | } 48 | 49 | .author { 50 | font-weight: bold; 51 | font-size: 15px; 52 | color: #43484D; 53 | letter-spacing: 0; 54 | line-height: 25px; 55 | margin-bottom: 0; 56 | } 57 | 58 | .profession { 59 | font-size: 13px; 60 | color: #86939E; 61 | letter-spacing: 0; 62 | line-height: 25px; 63 | } 64 | } 65 | 66 | @media (max-width: 576px) { 67 | .testimonials { 68 | text-align: center; 69 | 70 | .photo img { 71 | width: 100px; 72 | height: 100px; 73 | margin-top: 15px; 74 | } 75 | 76 | .quote { 77 | margin-top: 15px; 78 | } 79 | } 80 | } 81 | 82 | @media (max-width: 768px) { 83 | .testimonials { 84 | padding-top: 5px; 85 | 86 | .ant-carousel { 87 | .slick-slide { 88 | height: 320px; 89 | } 90 | 91 | .slick-dots { 92 | bottom: -15px; 93 | } 94 | } 95 | 96 | .photo img { 97 | margin-left: auto; 98 | margin-right: auto; 99 | float: none; 100 | } 101 | 102 | .quote { 103 | margin-top: 22px; 104 | line-height: 30px; 105 | font-size: 16px; 106 | height: auto; 107 | } 108 | 109 | .author { 110 | font-size: 14px; 111 | } 112 | 113 | .profession { 114 | font-size: 12px; 115 | } 116 | 117 | #org { 118 | display: none; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/components/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LzEditor from 'react-lz-editor'; 3 | 4 | import '../assets/styles/css/email.css'; 5 | 6 | 7 | class Editor extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | htmlContent: `

Good Morning Amy

13 |

This is placeholder information that the tool would generate. 14 | Perhaps something like this…I saw your contribution to a StackOverflow 15 | thread about sequencing Django South migrations in Django 1.4, 16 | you seem like you know your Django-fu. Buzzfeed is a social software company in New York 17 | (looks like you are just down the road) and I am building a Django team there.

18 |
19 | Add a custom note here:
20 | Want to grab coffee sometime next week? I have availability Tuesday and Thursday after 3 and like to meet at Ozo on Pearl st. 21 |
22 |
23 |

24 | Sam
25 | Engineering Manager, Buzzfeed
26 | Job URL 27 |

28 | ` 29 | }; 30 | 31 | this.receiveHtml = this.receiveHtml.bind(this); 32 | } 33 | 34 | receiveHtml = (content) => { 35 | if (this.state.htmlContent !== content) { 36 | this.props.onEdit(content); 37 | } 38 | } 39 | 40 | componentWillReceiveProps(nextProps) { 41 | if (nextProps.email !== this.props.email) { 42 | this.setState({ htmlContent: nextProps.email }); 43 | } 44 | } 45 | 46 | render() { 47 | return ( 48 | 57 | ); 58 | } 59 | } 60 | 61 | 62 | export default Editor; 63 | -------------------------------------------------------------------------------- /src/components/Email.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Col, Row, message } from 'antd'; 3 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 4 | 5 | import Header from './Header'; 6 | import Editor from "./Editor"; 7 | 8 | import { history } from '../store'; 9 | 10 | import '../assets/styles/css/email.css'; 11 | 12 | 13 | class Email extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | email: '' 20 | } 21 | } 22 | 23 | componentWillMount() { 24 | if (!this.props.resume.loading && !this.props.resume.generated_text) { 25 | history.push('/resume'); 26 | } 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps.resume.generated_text !== this.props.resume.generated_text) { 31 | this.setState({ email: nextProps.resume.generated_text }); 32 | message.success('Successfully generated the email'); 33 | } 34 | } 35 | 36 | onEditEmail = (email) => { 37 | this.setState({ copied: false, email: email.replace(/<[^>]+>/g, '') }); 38 | } 39 | 40 | generateEmail = () => { 41 | this.props.generateEmail({ 42 | candidate_json: JSON.stringify(this.props.resume.candidate_json), 43 | recruiter_json: JSON.stringify(this.props.resume.recruiter_json), 44 | }); 45 | } 46 | 47 | onClickCopy = () => { 48 | this.setState({ copied: true }); 49 | message.success('Successfully copied the email'); 50 | } 51 | 52 | render() { 53 | const { candidate_json } = this.props.resume; 54 | 55 | return ( 56 |
57 |
58 | 59 | 60 | 61 |

1 Email generated for:

62 | 63 |

{candidate_json.name}

64 |

{candidate_json.email}

65 | {/*

http://www.linkedin.com/in/amykeys

*/} 66 | 67 | {/**/} 68 | {/**/} 69 | {/**/} 70 | 71 | 72 |
73 | 74 | 75 | {/**/} 76 | 77 | 78 | 79 | 80 | 81 | {/**/} 82 | 87 |
88 | 89 | 90 |
91 |
92 | ); 93 | } 94 | } 95 | 96 | 97 | export default Email; 98 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col } from 'antd'; 3 | 4 | import logo from '../assets/img/coldoutreach-logo.png'; 5 | import '../assets/styles/css/header.css'; 6 | 7 | class Header extends Component { 8 | render() { 9 | return ( 10 |
11 | 12 | 13 | Coldoutreach Logo 14 | 15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default Header; 22 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col, Button, Icon } from 'antd'; 3 | import Header from './Header'; 4 | import Card from './landing/Card'; 5 | import Info from './landing/Info'; 6 | import Testimonial from './landing/Testimonial'; 7 | 8 | import '../assets/styles/css/home.css'; 9 | import candidate from '../assets/img/candidate.svg'; 10 | import generate from '../assets/img/generate.svg'; 11 | import email from '../assets/img/email.svg'; 12 | import recruit_1 from '../assets/img/recruit_1.svg'; 13 | import recruit_2 from '../assets/img/recruit_2.svg'; 14 | import recruit_3 from '../assets/img/recruit_3.svg'; 15 | import decor from '../assets/img/decor.svg'; 16 | 17 | class Home extends Component { 18 | render() { 19 | return ( 20 |
21 |
22 |
23 | 24 | 25 |

A Better Way to Find Talent

26 |

27 | Instantly create custom 28 | outreach emails tailored to 29 | each candidate. 30 |

31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | 51 | 52 |

Recruit the Smart Way

53 | 54 | 55 | 59 | 60 | 61 | 65 | 66 | 67 | 71 | 72 | 73 |
74 | 75 | 76 |
86 | ); 87 | } 88 | } 89 | 90 | export default Home; 91 | -------------------------------------------------------------------------------- /src/components/Resume.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Col, Icon, Row, Upload } from 'antd'; 3 | 4 | import Header from './Header'; 5 | 6 | import '../assets/styles/css/resume.css'; 7 | 8 | 9 | class Resume extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | candidate_file: null, 15 | recruiter_file: null, 16 | }; 17 | } 18 | 19 | onSubmit() { 20 | let data = new FormData(); 21 | 22 | data.append('candidate_file', this.state.candidate_file); 23 | data.append('recruiter_file', this.state.recruiter_file); 24 | 25 | this.props.uploadResume(data); 26 | } 27 | 28 | render() { 29 | const { resume } = this.props; 30 | 31 | const candidateProps = { 32 | beforeUpload: (file) => { 33 | this.setState({ 34 | candidate_file: file 35 | }); 36 | return false; 37 | }, 38 | onRemove: (file) => { 39 | this.setState({ candidate_file: null }); 40 | }, 41 | }; 42 | 43 | 44 | const recruiterProps = { 45 | beforeUpload: (file) => { 46 | this.setState({ 47 | recruiter_file: file 48 | }); 49 | return false; 50 | }, 51 | onRemove: (file) => { 52 | this.setState({ recruiter_file: null }); 53 | }, 54 | }; 55 | 56 | return ( 57 |
58 |
59 | 60 | 61 | 62 | 63 |

Upload Your Resume

64 | 65 | 66 | 70 | {!this.state.recruiter_file && ( 71 |
72 | 73 |
Upload
74 |
75 | )} 76 |
77 | 78 | 79 | 80 | 81 | 82 |

Upload Candidate Resume

83 | 84 | 85 | 89 | {!this.state.candidate_file && ( 90 |
91 | 92 |
Upload
93 |
94 | )} 95 |
96 | 97 | 98 | 99 | 100 | 103 | 104 | 105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | export default Resume; 112 | -------------------------------------------------------------------------------- /src/components/landing/Card.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import '../../assets/styles/css/card.css'; 4 | 5 | 6 | class Card extends Component { 7 | 8 | render() { 9 | const { title, image, alt } = this.props; 10 | const words = title.split(" "); 11 | return ( 12 |
13 | {alt}/ 14 |

{ words.map(word => {word}) }

15 |
16 | ); 17 | } 18 | 19 | } 20 | 21 | 22 | export default Card; 23 | -------------------------------------------------------------------------------- /src/components/landing/Info.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col } from 'antd'; 3 | 4 | import '../../assets/styles/css/info.css'; 5 | 6 | class Info extends Component { 7 | render() { 8 | const {id, image, alt, title, content} = this.props; 9 | 10 | return ( 11 | 12 | 13 | {alt}/ 14 |

{title}

15 | 16 | 17 |

{content}

18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | export default Info; 25 | -------------------------------------------------------------------------------- /src/components/landing/Slide.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col } from 'antd'; 3 | 4 | import '../../assets/styles/css/testimonial.css'; 5 | 6 | 7 | class Slide extends Component { 8 | 9 | render() { 10 | const {quote, img, org, alt, author, profession} = this.props; 11 | return( 12 | 13 | 14 | { org && 15 | 16 | } 17 | {alt} 18 | 19 | 20 | 21 |

{quote}

22 | 23 | 24 |

{author}

25 | 26 |

{profession}

27 |
28 | 29 | 30 |
31 | ) 32 | } 33 | 34 | } 35 | 36 | 37 | export default Slide; 38 | -------------------------------------------------------------------------------- /src/components/landing/Testimonial.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Carousel } from 'antd'; 3 | 4 | import Slide from './Slide'; 5 | 6 | import '../../assets/styles/css/testimonial.css'; 7 | 8 | import slack from '../../assets/img/slack.svg'; 9 | 10 | 11 | class Testimonial extends Component { 12 | 13 | render() { 14 | return ( 15 |
16 | 17 |
18 | 28 |
29 |
30 | 39 |
40 |
41 | 50 |
51 |
52 | 61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | } 68 | 69 | export default Testimonial; 70 | -------------------------------------------------------------------------------- /src/containers/Email.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Email from '../components/Email'; 4 | 5 | import * as ResumeActions from '../actions/index'; 6 | 7 | 8 | const mapStateToProps = (state, ownProps) => { 9 | return { ...state }; 10 | }; 11 | 12 | 13 | export default connect(mapStateToProps, { 14 | generateEmail: ResumeActions.GenerateEmail 15 | })(Email); 16 | -------------------------------------------------------------------------------- /src/containers/Home.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Home from '../components/Home'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { ...state }; 7 | }; 8 | 9 | const mapDispatchToProps = dispatch => { 10 | return {}; 11 | }; 12 | 13 | export default connect(mapStateToProps, mapDispatchToProps)(Home); 14 | -------------------------------------------------------------------------------- /src/containers/Resume.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Resume from '../components/Resume'; 4 | 5 | import * as ResumeActions from '../actions/index'; 6 | 7 | 8 | const mapStateToProps = (state, ownProps) => { 9 | return { ...state }; 10 | }; 11 | 12 | 13 | export default connect(mapStateToProps, { 14 | uploadResume: ResumeActions.UploadResume 15 | })(Resume); 16 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as EmailContainer } from './Email'; 2 | export { default as HomeContainer } from './Home'; 3 | export { default as ResumeContainer } from './Resume'; 4 | -------------------------------------------------------------------------------- /src/epics/index.js: -------------------------------------------------------------------------------- 1 | import { combineEpics } from 'redux-observable'; 2 | 3 | // Epics 4 | import { epics as resumeEpics } from './resume'; 5 | 6 | 7 | const rootEpic = combineEpics( 8 | resumeEpics 9 | ); 10 | 11 | 12 | export default rootEpic; 13 | -------------------------------------------------------------------------------- /src/epics/resume.js: -------------------------------------------------------------------------------- 1 | import { ActionsObservable, combineEpics } from 'redux-observable'; 2 | import { ajax } from 'rxjs/observable/dom/ajax'; 3 | 4 | // rxjs 5 | import 'rxjs/add/operator/catch'; 6 | import 'rxjs/add/operator/map'; 7 | import 'rxjs/add/observable/of'; 8 | import 'rxjs/add/operator/switchMap'; 9 | 10 | import * as Actions from '../actions'; 11 | import { history } from '../store'; 12 | 13 | 14 | const uploadResumeEpic = (action$) => 15 | action$ 16 | .ofType(Actions.UPLOAD_RESUME) 17 | .switchMap((action) => { 18 | return ajax.post(`/api/profile`, action.payload) 19 | .map((data) => { 20 | history.push('/email'); 21 | return Actions.UploadResumeSuccess(data.response) 22 | }) 23 | .catch((error) => ActionsObservable.of(Actions.Error(error))); 24 | }); 25 | 26 | 27 | const generateEmailEpic = (action$) => 28 | action$ 29 | .ofType(Actions.GENERATE_EMAIL) 30 | .switchMap((action) => { 31 | return ajax.post(`/api/email`, action.payload, { 'Content-Type': 'application/json' }) 32 | .map((data) => { 33 | return Actions.GenerateEmailSuccess(data.response.generated_text) 34 | }) 35 | .catch((error) => ActionsObservable.of(Actions.Error(error))); 36 | }); 37 | 38 | 39 | export const epics = combineEpics( 40 | generateEmailEpic, 41 | uploadResumeEpic, 42 | ); 43 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { ConnectedRouter } from 'react-router-redux'; 4 | 5 | // Providers 6 | import { LocaleProvider } from 'antd'; 7 | import { Provider as ReduxProvider } from 'react-redux'; 8 | import enUS from 'antd/lib/locale-provider/en_US'; 9 | 10 | import store, { history } from './store'; 11 | 12 | import App from './App'; 13 | 14 | import registerServiceWorker from './registerServiceWorker'; 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('coldoutreach') 25 | ); 26 | 27 | registerServiceWorker(); 28 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | 4 | import resumeReducer from './resume'; 5 | 6 | export default combineReducers({ 7 | resume: resumeReducer, 8 | routing: routerReducer 9 | }); 10 | -------------------------------------------------------------------------------- /src/reducers/resume.js: -------------------------------------------------------------------------------- 1 | import * as Actions from '../actions'; 2 | 3 | 4 | export default function reducer( 5 | state = { 6 | candidate_json: {}, 7 | generated_text: '', 8 | error: null, 9 | loading: false, 10 | recruiter_json: {}, 11 | }, 12 | action, 13 | ) { 14 | 15 | switch (action.type) { 16 | case Actions.UPLOAD_RESUME: 17 | case Actions.GENERATE_EMAIL: 18 | return { 19 | ...state, 20 | error: null, 21 | loading: true, 22 | }; 23 | 24 | 25 | case Actions.UPLOAD_RESUME_SUCCESS: 26 | return { 27 | candidate_json: JSON.parse(action.payload.candidate_json), 28 | generated_text: action.payload.generated_text, 29 | error: null, 30 | loading: false, 31 | recruiter_json: JSON.parse(action.payload.recruiter_json), 32 | }; 33 | 34 | 35 | case Actions.GENERATE_EMAIL_SUCCESS: 36 | return { 37 | ...state, 38 | error: null, 39 | generated_text: action.payload, 40 | loading: false, 41 | }; 42 | 43 | 44 | case Actions.ERROR: 45 | return { 46 | ...state, 47 | loading: false, 48 | error: action.payload, 49 | }; 50 | 51 | default: 52 | return state; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import * as Containers from './containers'; 2 | 3 | 4 | export const routes = [ 5 | { 6 | component: Containers.HomeContainer, 7 | exact: true, 8 | path: '/', 9 | }, 10 | { 11 | component: Containers.ResumeContainer, 12 | exact: true, 13 | path: '/resume', 14 | }, 15 | { 16 | component: Containers.EmailContainer, 17 | exact: true, 18 | path: '/email', 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import { createLogger } from 'redux-logger'; 4 | import createHistory from 'history/createBrowserHistory'; 5 | import { createEpicMiddleware } from 'redux-observable'; 6 | 7 | import reducers from './reducers'; 8 | 9 | // Epics 10 | import rootEpic from './epics'; 11 | 12 | 13 | export const history = createHistory(); 14 | 15 | 16 | // Epic Middleware 17 | const epicMiddleware = createEpicMiddleware(rootEpic); 18 | 19 | 20 | // Redux DevTools 21 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 22 | 23 | 24 | const store = createStore( 25 | reducers, 26 | composeEnhancers(applyMiddleware(createLogger(), routerMiddleware(history), epicMiddleware)) 27 | ); 28 | 29 | 30 | export default store; 31 | --------------------------------------------------------------------------------