-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: recommendation engine infrastructure
- Loading branch information
Showing
6 changed files
with
433 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }, | ||
], | ||
}, | ||
]; |
Oops, something went wrong.