Skip to content

Commit

Permalink
feat: recommendation engine infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
molant committed Sep 24, 2021
1 parent 5505f24 commit 5b398cd
Show file tree
Hide file tree
Showing 6 changed files with 433 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ module.exports = {
position: 'left',
label: 'Examples'
},
{
to:'/recommendation',
label: 'Recommendation engine',
position: 'left',
},
// {to: '/blog', label: 'Blog', position: 'left'},
{
href: 'https://github.com/crossplatform-dev/crossplatform.dev',
Expand Down
196 changes: 196 additions & 0 deletions src/components/RecommendationWizard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React, { useState } from 'react';
import styles from './RecommendationWizard.module.css';
import { questions } from '../data/questions.js';
import { technologies } from '../data/technologies.js';

const State = {
Waiting: 'waiting',
Questioning: 'questioning',
Ended: 'ended',
};

/**
* Based on the user's answers returns a list of technologies
* to look at in order of priority.
*/
const getRecommendations = (selectedTags) => {
const scoredTechnologies = [];
for (const technology of technologies) {
let score = 0;
let add = true;

for (const tag of selectedTags) {
const [feature, deal] = tag.split('-');
const weight = technology.categories[feature];
if ((deal === 'deal' && typeof weight === 'undefined') || weight === 0) {
// A 0 score on a category is a deal breaker
console.log(
`${technology.name} removed because of ${feature} is missing and it's a deal breaker`
);
add = false;
break;
}
score += weight || 0;
}

if (add) {
scoredTechnologies.push({
name: technology.name,
normalizedName: technology.normalizedName,
score,
});
}
}

const sortedTechnologies = scoredTechnologies.sort(
(technologyA, technologyB) => {
if (technologyA.score < technologyB.score) {
return 1;
}
if (technologyA.score == technologyB.score) {
return 0;
}

if (technologyA.score > technologyB.score) {
return -1;
}
}
);
return sortedTechnologies;
};

const FinalRecommendation = ({ restart, selections }) => {
const technologies = getRecommendations(selections);

return (
<div>
<p>
Based on your answers the technologies we think you should investigate
are:
</p>
<ul>
{technologies.map((technology) => {
return (
<li>
<a href={`/docs/${technology.normalizedName}`}>
{technology.name}
</a>
</li>
);
})}
</ul>
<button onClick={restart} className="button button--secondary">
Start again!
</button>

<p>
Doesn't seem right? Open an{' '}
<a href="https://github.com/crossplatform-dev/crossplatform.dev/issues/new">
issue
</a>{' '}
with more details!
</p>
</div>
);
};

/**
*
* @param {QuestioningProps} param0
* @returns
*/
const Questioning = ({ questions, done }) => {
const [question, setQuestion] = useState(questions[0]);
const [remainingQuestions, setRemainingQuestions] = useState(
questions.slice(1)
);
const [selectedTags, setTags] = useState([]);

/**
* Handles the selection changes of inputs in the form to make
* sure their state is updated in the React side.
*/
const handleChange = (e) => {
const { checked, value } = e.target;
if (value === 'none') {
return;
}
const indexOf = selectedTags.indexOf(value);

if (checked) {
if (indexOf === -1) {
setTags([...selectedTags, value]);
}
} else if (indexOf !== -1) {
selectedTags.splice(indexOf, 1);
setTags([...selectedTags]);
}
};

/**
* Updates the user's selection for the current question
* and moves to the next one or the final step.
*/
const handleSubmit = (evt) => {
evt.preventDefault();

if (remainingQuestions.length > 0) {
setQuestion(remainingQuestions[0]);
setRemainingQuestions(remainingQuestions.slice(1));
} else {
done(selectedTags);
}
};

return (
<form onSubmit={handleSubmit}>
<fieldset id="quiz">
<legend>{question.message}</legend>
{question.choices.map((choice) => {
const value = `${choice.value}-${
question.dealBreaker ? 'deal' : 'noDeal'
}`;
return (
<div key={choice.value}>
<input
type={question.type || 'radio'}
id={choice.value}
name="question"
value={value}
onChange={handleChange}
/>
<label htmlFor={choice.value}>{choice.name}</label>
<br />
</div>
);
})}
<button className="button button--secondary">Next</button>
</fieldset>
</form>
);
};

export default function RecommendationWizard() {
const [status, setState] = useState(State.Questioning);
const [selections, setSelections] = useState([]);

const done = (choices) => {
setSelections(choices);
setState(State.Ended);
};

const restart = () => {
setState(State.Questioning);
};

let section;
if (status === State.Waiting) {
section = <Intro setState={setState} />;
} else if (status === State.Questioning) {
section = <Questioning questions={questions} done={done} />;
} else if (status === State.Ended) {
section = <FinalRecommendation restart={restart} selections={selections} />;
}

return <article>{section}</article>;
}
20 changes: 20 additions & 0 deletions src/components/RecommendationWizard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
article {
display: flex;
flex-direction: row;
justify-content: center;
margin: 5em;
}

legend {
background-color: #000;
color: #fff;
padding: 3px 6px;
}

button {
margin: 0.5em 0;
}

li {
list-style: none;
}
83 changes: 83 additions & 0 deletions src/data/questions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Uses inquirer format (https://www.npmjs.com/package/inquirer#questions)
export const questions = [
{
category: 'platformSupport',
message:
'Does your application need to run on any mobile platform? (Select all that apply)',
type: 'checkbox',
dealBreaker: true,
choices: [
{ name: 'Android', value: 'android' },
{ name: 'iOS', value: 'ios' },
{ name: 'None', value: 'none' },
],
},
{
category: 'platformSupport',
message:
'Does your application need to run on any desktop platform? (Select all that apply)',
type: 'checkbox',
dealBreaker: true,
choices: [
{ name: 'Linux', value: 'linux' },
{ name: 'macOS', value: 'macos' },
{ name: 'Windows', value: 'windows' },
{ name: 'None', value: 'none' },
],
},
{
category: 'visual',
message:
'Do you want your application to have a consistent look across platforms or do you want it to look closer to the Operating System?',
choices: [
{ name: 'Consistent accross platforms', value: 'customUI' },
{ name: 'Match the OS look and feel', value: 'platformUI' },
{ name: 'Indifferent', value: 'none' },
],
},
{
category: 'fieldType',
message:
'Are you going to start a full new application or does it have to integrate with an existing one?',
choices: [
{ name: 'New application', value: 'greenfield' },
{ name: 'Existing application', value: 'brownfield' },
],
},
{
category: 'targetAudience',
message: 'Who will be the main user of your application?',
choices: [
{ name: 'Consumers', value: 'consumers' },
{ name: 'Enterprise users', value: 'enterprise' },
],
},
{
category: 'team',
notes: 'This depends mostly on enterprise users',
message:
'Is the application going to have a team working fulltime in the longterm?',
choices: [
{ name: 'Yes', value: 'longterm' },
{ name: 'No', value: 'shortterm' },
],
},
{
category: 'visual',
message:
"How visually complex or interactions is going to have your app's main view/page?",
choices: [
{ name: 'Simple layout or interactions', value: 'simpleLayout' },
{ name: 'Definitely not simple', value: 'complexLayout' },
],
},
{
category: 'support',
message:
'Do you think you will need to pay for support or would be help from the community be enough?',
choices: [
{ name: 'Paid support', value: 'paidSupport' },
{ name: 'Community', value: 'community' },
],
},
];
Loading

0 comments on commit 5b398cd

Please sign in to comment.