Add full profile search (#763)

* Prevent duplicate company names

* Fix output indentation

* Search full profile content using lunr.js

* Remove extra stop words

This wasn't really working correctly - the stop word 'work' would leave
instances of 'working' and 'works' in the index for example.

* Change company name description from "Name" to "Company name"

* Pre-process query:

- Search for terms in AND mode, per
  https://lunrjs.com/guides/searching.html#term-presence
- Discard non-alphanumeric characters from the search
- Better handling of contractions and searching for stop words

* Display search query and results in the console

* Add special search token: _incomplete

* Add a link to search for incomplete profiles

* Revert "Add a link to search for incomplete profiles"

This reverts commit f6384c90cb.

* Add link to search documentation

* Improve search explanation appearance when it spans multiple lines

* Fix searching for contractions

Previously, searching for e.g. "don't" wasn't working correctly. After
trimming the contraction, "do" is a stop word, so it should be ignored.

* Improve "empty search" message

* Prefer matches other than "company name" in search excerpts

* Move inline scripts before external scripts

This probably doesn't matter right now due to the way the scripts are
currently structured, but it might matter one day and it's more logical
this way.

* Fix search engine index progress

* Improve script indentation

* I got 99 problems and they're all bots

* Update script exit code

When a Node.js error occurs the exit code is probably going to be 1, so
we should use a different code.

* Fix the tests

* Update documentation

This was wrong (out of date), but the correct version is obvious from
reading the code.

* Make download progress work in both Chrome and Firefox

See https://stackoverflow.com/a/32799706
This commit is contained in:
James Nylen 2020-05-06 06:42:21 +00:00 committed by GitHub
parent bbdf527608
commit dac8b04fc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 629 additions and 125 deletions

View file

@ -9,7 +9,11 @@ const phin = require( 'phin' );
const rimraf = require( 'rimraf' );
const swig = require( 'swig-templates' );
const { parseFromDirectory, headingPropertyNames } = require( '../lib' );
const {
parseFromDirectory,
headingPropertyNames,
buildSearchData,
} = require( '../lib' );
const contentPath = path.join( __dirname, '..' );
const sitePath = path.join( __dirname, '..', 'site' );
const siteBuildPath = path.join( sitePath, 'build' );
@ -211,7 +215,7 @@ async function buildSite() {
// Set up styles/scripts for specific pages
const indexScripts = [ {
url: '//cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.min.js',
url: '//cdnjs.cloudflare.com/ajax/libs/lunr.js/2.3.7/lunr.min.js',
}, {
url: copyAssetToBuild( 'companies-table.js' ),
} ];
@ -223,6 +227,14 @@ async function buildSite() {
fs.copyFileSync( path.join( faviconPath, f ), path.join( siteBuildPath, f ) );
} );
// Generate search index
console.log( 'Generating search index' );
const searchIndexData = JSON.stringify( buildSearchData( data ) );
const searchIndexFilename = copyAssetToBuild(
'search.js',
searchIndexData
);
// Generate the index.html file from the main README
// TODO: Build this page and its table dynamically; more filters
const readmeTemplate = swig.compileFile(
@ -232,6 +244,11 @@ async function buildSite() {
writePage( 'index.html', readmeTemplate( {
stylesheets,
scripts: scripts.concat( indexScripts ),
inlineScripts: [
'\n\t\tvar searchIndexFilename = ' + JSON.stringify( searchIndexFilename ) + ';'
+ '\n\t\tvar searchIndexSize = ' + JSON.stringify( searchIndexData.length ) + ';'
+ '\n\t\t',
],
pageContent: data.readmeContent,
editUrl: githubEditUrl( 'README.md' ),
} ) );
@ -249,6 +266,7 @@ async function buildSite() {
writePage( path.join( dirname, 'index.html' ), companyTemplate( {
stylesheets,
scripts,
inlineScripts: [],
company,
headingPropertyNames,
missingHeadings,

View file

@ -47,4 +47,4 @@ console.log(
( errorCount === 1 ? '' : 's' )
);
process.exitCode = Math.min( errorCount, 99 );
process.exitCode = ( errorCount > 0 ? 3 : 0 );

View file

@ -5,6 +5,7 @@ const path = require( 'path' );
const util = require( 'util' );
const cheerio = require( 'cheerio' );
const lunr = require( 'lunr' );
const marked = require( 'marked' );
@ -75,10 +76,13 @@ exports.stripExtraChars = stripExtraChars;
/**
* Other exports
*/
exports.headingPropertyNames = headingsAll.reduce( ( acc, val ) => {
acc[ toIdentifierCase( val ) ] = val;
return acc;
}, {} );
function getHeadingPropertyNames() {
return headingsAll.reduce( ( acc, val ) => {
acc[ toIdentifierCase( val ) ] = val;
return acc;
}, {} );
}
exports.headingPropertyNames = getHeadingPropertyNames();
/**
@ -88,6 +92,7 @@ exports.headingPropertyNames = headingsAll.reduce( ( acc, val ) => {
* files, and validate and parse the content of the Markdown files.
*/
exports.parseFromDirectory = contentPath => {
const companyNamesSeen = {};
let errors = [];
function error( filename, msg, ...params ) {
@ -137,13 +142,14 @@ exports.parseFromDirectory = contentPath => {
let lastCompanyName = null;
$( 'tr' ).each( ( i, tr ) => {
const $tr = $( tr );
if ( i === 0 ) {
// Assign an ID to the table.
$( tr ).closest( 'table' ).attr( 'id', 'companies-table' );
$tr.closest( 'table' ).attr( 'id', 'companies-table' );
// Skip the table header row.
return;
}
const $td = $( tr ).children( 'td' );
const $td = $tr.children( 'td' );
const websiteUrl = $td.eq( 1 ).text();
const websiteText = websiteUrl
@ -161,10 +167,25 @@ exports.parseFromDirectory = contentPath => {
shortRegion: $td.eq( 2 ).text().trim(),
};
if ( ! readmeEntry.name ) {
if ( ! websiteText ) {
readmeError(
'Missing website for company: %s',
readmeEntry.name
);
}
if ( readmeEntry.name ) {
if ( companyNamesSeen[ readmeEntry.name.toLowerCase() ] ) {
readmeError(
'Duplicate company: %s',
readmeEntry.name
);
}
companyNamesSeen[ readmeEntry.name.toLowerCase() ] = true;
} else {
readmeError(
'Missing company name: %s',
$( tr ).html().replace( /\n/g, '' )
$tr.html().replace( /\n/g, '' )
);
}
@ -174,14 +195,14 @@ exports.parseFromDirectory = contentPath => {
) {
readmeError(
'Invalid content in Website column: %s',
$( tr ).html().replace( /\n/g, '' )
$tr.html().replace( /\n/g, '' )
);
}
if ( $td.eq( 2 ).children().length > 0 ) {
readmeError(
'Extra content in Region column: %s',
$( tr ).html().replace( /\n/g, '' )
$tr.html().replace( /\n/g, '' )
);
}
@ -235,7 +256,10 @@ exports.parseFromDirectory = contentPath => {
);
}
// Set classes on table cells
// Set identifying attributes on table elements
$tr
.attr( 'class', 'company-row' )
.attr( 'id', 'company-row-' + ( i - 1 ) );
$td.eq( 0 ).attr( 'class', 'company-name' );
$td.eq( 1 ).attr( 'class', 'company-website' );
$td.eq( 2 ).attr( 'class', 'company-region' );
@ -500,3 +524,65 @@ exports.parseFromDirectory = contentPath => {
readmeContent,
};
};
/**
* Build search index data from the result of parseFromDirectory().
*/
exports.buildSearchData = data => {
const textData = [];
data.companies.forEach( ( company, i ) => {
const thisTextData = {
id: String( i ),
nameText: company.name,
websiteText: company.websiteText,
};
if ( company.shortRegion ) {
thisTextData.shortRegion = company.shortRegion;
}
Object.keys( exports.headingPropertyNames ).forEach( h => {
if ( company.profileContent[ h ] ) {
const text = cheerio.load( company.profileContent[ h ] ).text()
// Replace warning emoji with a searchable token
.replace( /\u26a0/, '(_incomplete)' );
thisTextData[ h ] = text;
}
} );
textData.push( thisTextData );
} );
const index = lunr( function() {
this.field( 'nameText' );
this.field( 'websiteText' );
this.field( 'shortRegion' );
Object.keys( exports.headingPropertyNames ).forEach( h => {
this.field( h );
} );
// https://github.com/olivernn/lunr.js/issues/25#issuecomment-623267494
this.metadataWhitelist = ['position'];
// https://github.com/olivernn/lunr.js/issues/192#issuecomment-172915226
// https://gist.github.com/olivernn/7cd496f8654a0246c53c
function contractionTrimmer( token ) {
return token.update( str => {
return str.replace( /('m|'ve|n't|'d|'ll|'ve|'s|'re)$/, '' );
} );
}
lunr.Pipeline.registerFunction( contractionTrimmer, 'contractionTrimmer' );
this.pipeline.after( lunr.trimmer, contractionTrimmer );
Object.keys( textData ).forEach( c => this.add( textData[ c ] ) );
} );
const headings = getHeadingPropertyNames();
headings.nameText = 'Company name';
headings.websiteText = 'Website';
headings.shortRegion = 'Region';
return { index, textData, headings };
};

5
package-lock.json generated
View file

@ -627,6 +627,11 @@
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
},
"lunr": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.7.tgz",
"integrity": "sha512-HjFSiy0Y0qZoW5OA1I6qBi7OnsDdqQnaUr03jhorh30maQoaP+4lQCKklYE3Nq3WJMSUfuBl6N+bKY5wxCb9hw=="
},
"marked": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",

View file

@ -14,6 +14,7 @@
},
"dependencies": {
"cheerio": "^1.0.0-rc.3",
"lunr": "2.3.7",
"marked": "^0.7.0",
"phin": "^3.4.0",
"rimraf": "^3.0.0",

View file

@ -1,52 +1,222 @@
function setupFilters() {
function setupSearch() {
var table = document.querySelector( 'table#companies-table' );
var headerCells = table.querySelectorAll( 'thead tr th' );
headerCells[ 0 ].innerHTML =
'<button class="sort" data-sort="company-name">Name</button>';
headerCells[ 1 ].innerHTML =
'<button class="sort" data-sort="company-website">Website</button>';
headerCells[ 2 ].innerHTML =
'<button class="sort" data-sort="company-region">Region</button>';
var searchInput = document.createElement( 'input' );
searchInput.type = 'text';
searchInput.placeholder = 'Search';
searchInput.id = 'search-input';
var tbody = table.querySelector( 'tbody' );
tbody.setAttribute( 'class', 'list' );
var filterInput = document.createElement( 'input' );
filterInput.type = 'text';
filterInput.placeholder = 'Filter Companies';
filterInput.id = 'company-filter';
filterInput.setAttribute( 'class', 'company-filter' );
var searchStatus = document.createElement( 'span' );
searchStatus.id = 'search-status';
var companiesHeading = document.querySelector( 'h2#companies' );
companiesHeading.appendChild( filterInput );
companiesHeading.appendChild( searchInput );
companiesHeading.appendChild( searchStatus );
var filtersExplanation = document.createElement( 'p' );
filtersExplanation.id = 'filters-explanation';
filtersExplanation.innerHTML = (
'Use the text box above to filter the list of companies, '
+ 'or click a column heading to sort by that column.'
var searchExplanation = document.createElement( 'p' );
searchExplanation.id = 'search-explanation';
searchExplanation.innerHTML = (
'Use the text box above to search all of our company data. '
+ ' <a href="https://blog.remoteintech.company/search-help/">More info</a>'
);
table.parentNode.insertBefore( filtersExplanation, table );
table.parentNode.insertBefore( searchExplanation, table );
window.tableFilter = new List(
'main', // element ID that contains everything
{
valueNames: [
'company-name',
'company-website',
'company-region'
],
searchClass: 'company-filter',
var searchLoading = false;
var searchData = null;
var searchIndex = null;
var updateTimeout = null;
function updateSearch() {
if ( ! searchData || searchLoading ) {
return;
}
);
var searchValue = searchInput.value
.replace( /[^a-z0-9_']+/gi, ' ' )
.trim()
.split( ' ' )
.map( function( term ) {
term = term
.replace( /('m|'ve|n't|'d|'ll|'ve|'s|'re)$/, '' )
.replace( /'/g, '' );
if ( ! lunr.stopWordFilter( term.toLowerCase() ) ) {
return null;
} else if ( term ) {
return '+' + term;
} else {
return term;
}
} )
.filter( Boolean )
.join( ' ' );
var allMatch = ! searchValue;
var searchResults = searchValue ? searchIndex.search( searchValue ) : [];
var searchDisplayValue = (
searchValue === '+_incomplete'
? 'Incomplete profile'
: searchInput.value.trim()
);
if ( allMatch ) {
searchStatus.innerHTML = (
'Empty search; showing all '
+ searchData.textData.length
+ ' companies'
);
} else if ( searchResults.length === 1 ) {
searchStatus.innerText = searchDisplayValue + ': 1 result';
} else {
searchStatus.innerText = (
searchDisplayValue + ': '
+ searchResults.length + ' results'
);
}
var searchMatches = {};
searchResults.forEach( function( r ) {
searchMatches[ +r.ref ] = r;
} );
if ( window.console && console.log ) {
console.log( 'search', { value: searchValue, results: searchResults } );
}
searchData.textData.forEach( function( company, index ) {
var match = searchMatches[ index ];
var row = document.getElementById( 'company-row-' + index );
var rowMatch = row.nextElementSibling;
if ( rowMatch && rowMatch.classList.contains( 'company-match' ) ) {
rowMatch.parentNode.removeChild( rowMatch );
}
row.style.display = ( match || allMatch ? '' : 'none' );
row.classList.remove( 'has-match' );
if ( match ) {
row.classList.add( 'has-match' );
var metadata = match.matchData.metadata;
var contextWords = ( window.innerWidth <= 600 ? 4 : 6 );
var k1, k2, pos;
loop1: for ( k1 in metadata ) {
for ( k2 in metadata[ k1 ] ) {
pos = metadata[ k1 ][ k2 ].position[ 0 ];
if ( k2 !== 'nameText' ) {
// Accept company name for matches, but prefer
// other fields if there are any
break loop1;
}
}
}
rowMatch = document.createElement( 'tr' );
rowMatch.setAttribute( 'class', 'company-match' );
var rowMatchCell = document.createElement( 'td' );
rowMatchCell.setAttribute( 'colspan', 3 );
var spanBefore = document.createElement( 'span' );
var spanMatch = document.createElement( 'strong' );
var spanAfter = document.createElement( 'span' );
var text = company[ k2 ];
var words = [];
var currentWord = '';
var i, inWord, c;
for ( i = pos[ 0 ] - 1; i >= 0; i-- ) {
c = text.substring( i, i + 1 );
inWord = /\S/.test( c );
if ( inWord ) {
currentWord = c + currentWord;
}
if ( ( ! inWord || i === 0 ) && currentWord ) {
words.unshift( currentWord );
currentWord = '';
if ( words.length === contextWords + 1 ) {
words[ 0 ] = '\u2026';
break;
}
}
}
spanBefore.innerText = (
( window.innerWidth > 600 ? searchData.headings[ k2 ] + ': ' : '' )
+ words.join( ' ' )
+ ' '
).replace( /\(_incomplete\)/, '(Incomplete)' );
spanMatch.innerText = text
.substring( pos[ 0 ], pos[ 0 ] + pos[ 1 ] )
.replace( /\(_incomplete\)/, '(Incomplete)' );
words = [];
currentWord = '';
for ( i = pos[ 0 ] + pos[ 1 ] + 1; i < text.length; i++ ) {
c = text.substring( i, i + 1 );
inWord = /\S/.test( c );
if ( inWord ) {
currentWord += c;
}
if ( ( ! inWord || i === text.length - 1 ) && currentWord ) {
words.push( currentWord );
currentWord = '';
if ( words.length === contextWords + 1 ) {
words[ contextWords ] = '\u2026';
break;
}
}
}
spanAfter.innerText = (
' ' + words.join( ' ' )
).replace( /\(_incomplete\)/, '(Incomplete)' );
rowMatchCell.appendChild( spanBefore );
rowMatchCell.appendChild( spanMatch );
rowMatchCell.appendChild( spanAfter );
rowMatch.appendChild( rowMatchCell );
row.parentNode.insertBefore( rowMatch, row.nextSibling );
}
} );
}
searchInput.addEventListener( 'focus', function() {
if ( searchData || searchLoading ) {
return;
}
searchLoading = true;
var searchLoadingText = 'Loading search data...';
searchStatus.innerHTML = searchLoadingText;
var xhr = new XMLHttpRequest();
xhr.open( 'GET', searchIndexFilename );
xhr.onprogress = function( e ) {
var percentLoaded;
if ( e.lengthComputable ) {
percentLoaded = Math.round( 100 * e.loaded / e.total );
} else {
percentLoaded = Math.min(
100,
Math.round( 100 * e.loaded / searchIndexSize )
);
}
searchStatus.innerHTML = searchLoadingText + ' ' + percentLoaded + '%';
};
xhr.onload = function() {
searchLoading = false;
if ( xhr.status === 200 ) {
searchData = JSON.parse( xhr.response );
searchIndex = lunr.Index.load( searchData.index );
updateSearch();
} else {
searchStatus.innerHTML = 'Error!';
}
};
xhr.send();
} );
searchInput.addEventListener( 'keyup', function() {
if ( updateTimeout ) {
clearTimeout( updateTimeout );
}
updateTimeout = setTimeout( updateSearch, 450 );
} );
document.body.setAttribute(
'class',
document.body.getAttribute( 'class' ) + ' filters-enabled'
document.body.getAttribute( 'class' ) + ' search-enabled'
);
}
document.addEventListener( 'DOMContentLoaded', function( event ) {
setupFilters();
setupSearch();
} );

View file

@ -46,10 +46,10 @@ h1.company-name {
}
/**
* Styles for the companies table and filters on the main page
* Styles for the companies table and search on the main page
*/
#company-filter {
#search-input {
margin: 0 0 0 16px;
padding: 8px;
font-family: "Source Sans Pro", sans-serif;
@ -58,60 +58,47 @@ h1.company-name {
vertical-align: middle;
}
#filters-explanation {
#search-status {
margin-left: 18px;
font-size: 15px;
font-weight: normal;
}
#search-explanation {
font-style: italic;
font-size: 15px;
line-height: 18px;
}
@media screen and (min-width: 50em) {
#filters-explanation {
#search-explanation {
font-size: 16px;
}
}
body.filters-enabled table#companies-table th {
padding: 0;
table#companies-table th {
border-bottom: 2px solid #eee;
line-height: 1;
padding: 9px 6px;
}
table#companies-table button.sort {
width: 100%;
table#companies-table td {
border-width: 0;
border-radius: 0;
padding: 7px 3px 4px;
text-align: left;
font-weight: 700;
color: #666;
outline: none;
}
table#companies-table button.sort:hover,
table#companies-table button.sort:focus {
color: #c61610;
}
table#companies-table button.sort:hover {
background: #f4f4f4;
line-height: 1;
}
/* Sort indicators adapted from http://listjs.com/examples/table/ */
table#companies-table .sort.asc:after,
table#companies-table .sort.desc:after {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
content: '';
display: inline-block;
position: relative;
left: 3px;
top: -2px;
table#companies-table tr.company-row td {
border-top: 1px solid #eee;
padding: 9px 6px;
}
table#companies-table tr.company-row.has-match td {
padding-bottom: 3px;
}
table#companies-table .sort.asc:after {
border-bottom: 6px solid;
}
table#companies-table .sort.desc:after {
border-top: 6px solid;
table#companies-table tr.company-match td {
padding: 0 0 9px 18px;
font-size: 81%;
font-style: italic;
}
/* Column-specific styles */
@ -132,16 +119,21 @@ table#companies-table td.company-region {
/* Mobile-friendly display */
@media screen and (max-width: 600px) {
body.filters-enabled h2#companies {
margin-bottom: 18px;
body.search-enabled h2#companies {
margin-bottom: 12px;
}
#company-filter {
#search-input {
display: block;
width: 100%;
margin: 27px 0 0 0;
}
#search-status {
font-size: 13.5px;
margin-left: 0;
}
table#companies-table,
table#companies-table thead,
table#companies-table tbody,
@ -149,33 +141,31 @@ table#companies-table td.company-region {
display: block;
}
table#companies-table tr {
border-bottom: 1px solid #eee;
padding: 0 0 8px 12px;
table#companies-table tr.company-row {
border-top: 1px solid #eee;
padding: 7.5px 0 6px 12px;
line-height: 1.2;
}
table#companies-table tr.company-row.has-match {
padding-bottom: 3px;
}
table#companies-table button.sort {
width: auto;
padding-left: 6px;
padding-right: 6px;
}
table#companies-table .sort.asc:after,
table#companies-table .sort.desc:after {
margin-right: 2px;
table#companies-table tr.company-match td {
padding-left: 12px;
font-size: 90%;
}
table#companies-table thead tr {
border-bottom-width: 3px;
border-bottom: 2px solid #eee;
padding: 0;
}
table#companies-table th,
table#companies-table td {
table#companies-table tr.company-row td,
table#companies-table tr.company-row.has-match td {
width: auto !important;
padding-left: 0;
padding-right: 0;
border-bottom-width: 0;
padding: 0;
border-width: 0;
}
table#companies-table th {
@ -191,11 +181,12 @@ table#companies-table td.company-region {
display: none;
}
table#companies-table td.company-name {
table#companies-table tr.company-row td.company-name,
table#companies-table tr.company-row.has-match td.company-name {
display: flex;
font-size: 16px;
font-weight: 700;
padding-bottom: 0;
padding-bottom: 3px;
margin-left: -12px;
}

View file

@ -6,10 +6,13 @@
<link rel="shortcut icon" href="/assets/remoteintech.png" type="image/x-icon" />
<title>{% block pageTitle %}{% endblock %} &#8211; Remote In Tech</title>
{%- for stylesheet in stylesheets %}
<link rel="stylesheet" type="text/css" href="{{ stylesheet.url }}" />
<link rel="stylesheet" type="text/css" href="{{ stylesheet.url }}" />
{%- endfor %}
{%- for src in inlineScripts %}
<script type="text/javascript">{{ src|safe }}</script>
{%- endfor %}
{%- for script in scripts %}
<script type="text/javascript" src="{{ script.url }}"></script>
<script type="text/javascript" src="{{ script.url }}"></script>
{%- endfor %}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png">

View file

@ -0,0 +1,13 @@
# Test data
This company table and its linked company profiles contain fully valid data.
## Companies
Name | Website | Region
------------ | ------- | -------
[&yet](/company-profiles/and-yet.md) | https://andyet.com | Worldwide
[&Yet](/company-profiles/and.md) | https://andyet.com | Worldwide
[10up](/company-profiles/10up.md) | https://10up.com/ | Worldwide
[17hats](/company-profiles/17hats.md) | https://www.17hats.com/ | Worldwide
[18F](/company-profiles/18f.md) | https://18f.gsa.gov/ | USA

View file

@ -0,0 +1,39 @@
# 10up, A WordPress Development Agency
## Company blurb
We make websites and content management simple and fun with premiere web design & development consulting services, by contributing to open platforms like WordPress, and by providing tools and products (like [PushUp](https://pushupnotifications.com/)) that make web publishing a cinch.
At 10up, we dont just “make” things we engineer them. Were a group of people built to solve problems; made to create; wired to delight. From beautiful pixels to beautiful code, we constantly improve the things around us, applying our passions to our clients projects and goals.
Weve had the privilege of working on big web projects for clients as diverse as TechCrunch, ESPNs FiveThirtyEight and Grantland, SurveyMonkey, Junior Diabetes Research Foundation (JDRF), and Google.
## Company size
125 and growing, spread across web engineering, systems, design, project management, strategy/accounts, and operations.
## Remote status
10up didnt integrate remote work we intended to be remote from the start! Being remote allows us to find talent no matter where they're located, scale up to meet needs with relative fluidity, and have been bootstrapped from the start. We also recognize the challenges of working remotely, and put a lot of effort into in-person meetups, communication tools, and ensuring that employees have the benefits and support they need no matter where they are.
## Region
We have employees all around the world, from across the US to the UK to South Africa to the Philippines. Most are currently located in North America, a number travel frequently, and some even work nomadically.
## Company technologies
* WordPress
* PHP
* Sass
* Git
* Vagrant
* Nginx
* Memcache
## Office locations
None; or everywhere!
## How to apply
Check out our [careers page](https://10up.com/careers/) and send an email to jobs@10up.com. Our amazing Recruitment Manager Christine Garrison will be on the other end.

View file

@ -0,0 +1,25 @@
# 17hats
## Company blurb
Are you ready to stop juggling multiple apps to manage your business? From booking clients, to managing projects, to controlling your finances, with 17hats you have everything you need to manage your business and clients anytime, anywhere. Pretty neat if we say so ourselves! We created 17hats specifically for “businesses of one” with a focus on simplicity and ease of use.
## Company size
11-50
## Remote status
Every engineer on our team works remotely. That being said, we do need someone who can work California business hours (9am-6pm PST) and who is available for emergencies at night. If you live in the LA area, you are welcome to be in our cool office in Pasadena. Bonus points if you can curse in Dutch.
## Region
Worldwide
## Company technologies
iOS, React, Knockout, Rails, Perl, HTML, Sql, Ruby, JQuery
## How to apply
Email buford@17hats.com with github and/or CV

View file

@ -0,0 +1,55 @@
# 18F
## Company blurb
18F is a civic consultancy for the government, inside the government, working with agencies to rapidly deploy tools and services that are easy to use, cost efficient, and reusable. Our goal is to change how the government buys and develops digital services by helping agencies adopt modern techniques that deliver superior products.
We are transforming government from the inside out, creating cultural change by working with teams inside agencies who want to create great services for the public.
We are a trusted partner for agencies working to transform how they build and buy tools and services in a user-centered way.
We will accomplish our mission by:
putting the needs of the public first
being design-centric, agile, open, and data-driven
deploying tools and services early and often
## Company size
100+
## Remote status
18F employees live all over the country. We work out of homes in Dayton and Tucson, St. Louis and Chapel Hill — and in federal buildings in San Francisco, Chicago, New York City, and Washington D.C.
That means many of our project teams are also made up of distributed employees working all over the country. For example, you may have a developer in Austin, a designer in Washington D.C., and a content strategist in Portland — but theyre all working on the same team and with the same partners.
Because we work on distributed teams so frequently, we've developed certain strategies for working well as a collaborative operation.
[We have a “remote first” mindset.](https://18f.gsa.gov/2015/10/15/best-practices-for-distributed-teams/)
## Region
U.S. citizens, non-citizens who are nationals of the U.S., or people who have been admitted to the U.S. for permanent residence and hold a valid green card.
## Company technologies
Ruby, Python, HTML, CSS, JavaScript
## Office locations
Federal buildings in San Francisco, Chicago, New York City, and Washington D.C.
## How to apply
[Open positions](https://pages.18f.gov/joining-18f/open-positions/)
If you want to apply directly to 18F please email join18f@gsa.gov. We dont require a formal cover letter, but let us know more about you:
Send your current resume, preferably as a PDF.
Link to your GitHub profile, design portfolio, or attach a writing sample.
Specify what role youd like to be considered for. Check out our openings here.
If you're a Veteran of the U.S. Armed Forces or if you are eligible for "derived" preference, please mention that in your email so we can give you priority consideration.
Don't see an opening that suits you? Tell us what you want to do!
[How to apply](https://pages.18f.gov/joining-18f/how-to-apply/)

View file

@ -0,0 +1,44 @@
# &yet (And Yet)
## Company blurb
[&yet](https://andyet.com) is about people. Were known as a design and development consultancy (specializing in Node, React, and realtime), but we dont fit neatly in a box.
We design and [develop custom software](https://andyet.com/software) for web, mobile, desktop, chat, and voice.
We enable millions of people to make super simple video calls with [Talky](https://talky.io).
We pioneer software and standards for [realtime communications](https://andyet.com/realtime).
We [wrote the book](https://gatherthepeople.com) on taking a human approach to marketing for people who would rather make what they love than persuade people to buy it.
We create high-impact conference experiences such as [RealtimeConf](http://experience.realtimeconf.com) and more recently[&yetConf](http://andyetconf.com).
[Learn more about our team](https://andyet.com/about).
## Company size
20+
## Remote status
We employ several strategies to ensure an inclusive and collaborative environment for all our employees.
To communicate we use [Slack](https://slack.com) (text-chat), our own product [Talky](https://talky.io) (video chat and meetings), [Twist](https://twistapp.com) (daily check-ins) and [GitHub](https://github.com) (organization wide discussions).
One-on-ones and bi-weekly company-wide updates are a crucial part of staying connected and understanding our team as things change. We encourage employees to use these meetings to bring up frustrations, ideas, or whatever they need in order to be their best selves and to do their best work.
At least once a year we organize an in-person all-hands team week. Its the best.
## Region
&yet has one office located in Richland, WA. Currently ten people are working remotely out of Seattle, Portland, Folsom, Phoenix, Denver, Kansas City, Frankfurt, Oslo, and Melbourne. The most significant timezone difference is 17 hours.
## Company technologies
* Node.js
* React
* WebRTC
* Pug
* Stylus
## Office locations
[Fuse Coworking in Richland, WA](https://goo.gl/maps/oJaAQFf12tv)
## How to apply
No current openings.

View file

@ -0,0 +1,44 @@
# &yet (And Yet)
## Company blurb
[&yet](https://andyet.com) is about people. Were known as a design and development consultancy (specializing in Node, React, and realtime), but we dont fit neatly in a box.
We design and [develop custom software](https://andyet.com/software) for web, mobile, desktop, chat, and voice.
We enable millions of people to make super simple video calls with [Talky](https://talky.io).
We pioneer software and standards for [realtime communications](https://andyet.com/realtime).
We [wrote the book](https://gatherthepeople.com) on taking a human approach to marketing for people who would rather make what they love than persuade people to buy it.
We create high-impact conference experiences such as [RealtimeConf](http://experience.realtimeconf.com) and more recently[&yetConf](http://andyetconf.com).
[Learn more about our team](https://andyet.com/about).
## Company size
20+
## Remote status
We employ several strategies to ensure an inclusive and collaborative environment for all our employees.
To communicate we use [Slack](https://slack.com) (text-chat), our own product [Talky](https://talky.io) (video chat and meetings), [Twist](https://twistapp.com) (daily check-ins) and [GitHub](https://github.com) (organization wide discussions).
One-on-ones and bi-weekly company-wide updates are a crucial part of staying connected and understanding our team as things change. We encourage employees to use these meetings to bring up frustrations, ideas, or whatever they need in order to be their best selves and to do their best work.
At least once a year we organize an in-person all-hands team week. Its the best.
## Region
&yet has one office located in Richland, WA. Currently ten people are working remotely out of Seattle, Portland, Folsom, Phoenix, Denver, Kansas City, Frankfurt, Oslo, and Melbourne. The most significant timezone difference is 17 hours.
## Company technologies
* Node.js
* React
* WebRTC
* Pug
* Stylus
## Office locations
[Fuse Coworking in Richland, WA](https://goo.gl/maps/oJaAQFf12tv)
## How to apply
No current openings.

View file

@ -12,37 +12,37 @@ website links.</p>
<th>Region</th>
</tr>
</thead>
<tbody><tr>
<tbody><tr class="company-row" id="company-row-0">
<td class="company-name"><a href="/and-yet/">&amp;yet</a></td>
<td class="company-website"><a href="https://andyet.com" target="_blank" rel="noopener noreferrer">andyet.com</a></td>
<td class="company-region">Worldwide</td>
</tr>
<tr>
<tr class="company-row" id="company-row-1">
<td class="company-name"><a href="/10up/">10up</a></td>
<td class="company-website"><a href="https://10up.com/" target="_blank" rel="noopener noreferrer">10up.com</a></td>
<td class="company-region">Worldwide</td>
</tr>
<tr>
<tr class="company-row" id="company-row-2">
<td class="company-name"><a href="/17hats/">17hats</a></td>
<td class="company-website"><a href="https://www.17hats.com/" target="_blank" rel="noopener noreferrer">17hats.com</a></td>
<td class="company-region">Worldwide</td>
</tr>
<tr>
<tr class="company-row" id="company-row-3">
<td class="company-name"><a href="/18f/">18F</a></td>
<td class="company-website"><a href="https://18f.gsa.gov/" target="_blank" rel="noopener noreferrer">18f.gsa.gov</a></td>
<td class="company-region">USA</td>
</tr>
<tr>
<tr class="company-row" id="company-row-4">
<td class="company-name"><a href="/45royale/">45royale</a> &#x26A0;</td>
<td class="company-website"><a href="http://45royale.com/" target="_blank" rel="noopener noreferrer">45royale.com</a></td>
<td class="company-region"></td>
</tr>
<tr>
<tr class="company-row" id="company-row-5">
<td class="company-name"><a href="/aerolab/">Aerolab</a> &#x26A0;</td>
<td class="company-website"><a href="https://aerolab.co/" target="_blank" rel="noopener noreferrer">aerolab.co</a></td>
<td class="company-region"></td>
</tr>
<tr>
<tr class="company-row" id="company-row-6">
<td class="company-name"><a href="/angularclass/">AngularClass</a> &#x26A0;</td>
<td class="company-website"><a href="http://www.wikihow.com/wikiHow:About-wikiHow" target="_blank" rel="noopener noreferrer">wikihow.com/wikiHow:About-wikiHow</a></td>
<td class="company-region">PST Timezone</td>

View file

@ -54,9 +54,7 @@ exports.runValidationScriptWithFixtures = ( dirName, env = {} ) => {
const output = result.stdout.toString().trim().split( '\n' );
const exitCode = result.status;
expect( output[ output.length - 1 ] ).to.equal(
exitCode + ' problem' + ( exitCode === 1 ? '' : 's' ) + ' detected'
);
let errorSummary = output[ output.length - 1 ];
if ( output.length >= 2 ) {
expect( output[ output.length - 2 ] ).to.equal( '' );
output.splice( -2 );
@ -70,5 +68,5 @@ exports.runValidationScriptWithFixtures = ( dirName, env = {} ) => {
} );
}
return { output, exitCode };
return { output, errorSummary, exitCode };
};

View file

@ -39,6 +39,15 @@ describe( 'validation errors', () => {
} );
} );
it( 'should catch duplicate company names', () => {
expectValidateFixturesResult( 'duplicate-company', {
errorCount: 1,
output: [
'README.md: Duplicate company: &Yet',
]
} );
} );
it( 'should catch unsorted company names', () => {
expectValidateFixturesResult( 'unsorted', {
errorCount: 2,

View file

@ -6,6 +6,7 @@ describe( 'validation script (integration tests)', () => {
it( 'should pass with valid data', () => {
expect( runValidationScriptWithFixtures( 'valid' ) ).to.eql( {
exitCode: 0,
errorSummary: '0 problems detected',
output: [],
} );
} );
@ -14,6 +15,7 @@ describe( 'validation script (integration tests)', () => {
const env = { REPORT_PROFILE_HEADINGS: 'y' };
expect( runValidationScriptWithFixtures( 'valid-incomplete', env ) ).to.eql( {
exitCode: 0,
errorSummary: '0 problems detected',
output: [
'Profile headings by count (7 total profiles):',
'Company blurb: 7',
@ -30,7 +32,8 @@ describe( 'validation script (integration tests)', () => {
it( 'should catch unsorted company names, and count headings', () => {
const env = { REPORT_PROFILE_HEADINGS: 'y' };
expect( runValidationScriptWithFixtures( 'unsorted', env ) ).to.eql( {
exitCode: 2,
exitCode: 3,
errorSummary: '2 problems detected',
output: [
'README.md: Company is listed out of order: "17hats" (should be before "18F")',
'README.md: Company is listed out of order: "&yet" (should be before "17hats")',