Skip to content

Commit 5b398cd

Browse files
committed
feat: recommendation engine infrastructure
1 parent 5505f24 commit 5b398cd

File tree

6 files changed

+433
-0
lines changed

6 files changed

+433
-0
lines changed

docusaurus.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ module.exports = {
4040
position: 'left',
4141
label: 'Examples'
4242
},
43+
{
44+
to:'/recommendation',
45+
label: 'Recommendation engine',
46+
position: 'left',
47+
},
4348
// {to: '/blog', label: 'Blog', position: 'left'},
4449
{
4550
href: 'https://github.com/crossplatform-dev/crossplatform.dev',
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import React, { useState } from 'react';
2+
import styles from './RecommendationWizard.module.css';
3+
import { questions } from '../data/questions.js';
4+
import { technologies } from '../data/technologies.js';
5+
6+
const State = {
7+
Waiting: 'waiting',
8+
Questioning: 'questioning',
9+
Ended: 'ended',
10+
};
11+
12+
/**
13+
* Based on the user's answers returns a list of technologies
14+
* to look at in order of priority.
15+
*/
16+
const getRecommendations = (selectedTags) => {
17+
const scoredTechnologies = [];
18+
for (const technology of technologies) {
19+
let score = 0;
20+
let add = true;
21+
22+
for (const tag of selectedTags) {
23+
const [feature, deal] = tag.split('-');
24+
const weight = technology.categories[feature];
25+
if ((deal === 'deal' && typeof weight === 'undefined') || weight === 0) {
26+
// A 0 score on a category is a deal breaker
27+
console.log(
28+
`${technology.name} removed because of ${feature} is missing and it's a deal breaker`
29+
);
30+
add = false;
31+
break;
32+
}
33+
score += weight || 0;
34+
}
35+
36+
if (add) {
37+
scoredTechnologies.push({
38+
name: technology.name,
39+
normalizedName: technology.normalizedName,
40+
score,
41+
});
42+
}
43+
}
44+
45+
const sortedTechnologies = scoredTechnologies.sort(
46+
(technologyA, technologyB) => {
47+
if (technologyA.score < technologyB.score) {
48+
return 1;
49+
}
50+
if (technologyA.score == technologyB.score) {
51+
return 0;
52+
}
53+
54+
if (technologyA.score > technologyB.score) {
55+
return -1;
56+
}
57+
}
58+
);
59+
return sortedTechnologies;
60+
};
61+
62+
const FinalRecommendation = ({ restart, selections }) => {
63+
const technologies = getRecommendations(selections);
64+
65+
return (
66+
<div>
67+
<p>
68+
Based on your answers the technologies we think you should investigate
69+
are:
70+
</p>
71+
<ul>
72+
{technologies.map((technology) => {
73+
return (
74+
<li>
75+
<a href={`/docs/${technology.normalizedName}`}>
76+
{technology.name}
77+
</a>
78+
</li>
79+
);
80+
})}
81+
</ul>
82+
<button onClick={restart} className="button button--secondary">
83+
Start again!
84+
</button>
85+
86+
<p>
87+
Doesn't seem right? Open an{' '}
88+
<a href="https://github.com/crossplatform-dev/crossplatform.dev/issues/new">
89+
issue
90+
</a>{' '}
91+
with more details!
92+
</p>
93+
</div>
94+
);
95+
};
96+
97+
/**
98+
*
99+
* @param {QuestioningProps} param0
100+
* @returns
101+
*/
102+
const Questioning = ({ questions, done }) => {
103+
const [question, setQuestion] = useState(questions[0]);
104+
const [remainingQuestions, setRemainingQuestions] = useState(
105+
questions.slice(1)
106+
);
107+
const [selectedTags, setTags] = useState([]);
108+
109+
/**
110+
* Handles the selection changes of inputs in the form to make
111+
* sure their state is updated in the React side.
112+
*/
113+
const handleChange = (e) => {
114+
const { checked, value } = e.target;
115+
if (value === 'none') {
116+
return;
117+
}
118+
const indexOf = selectedTags.indexOf(value);
119+
120+
if (checked) {
121+
if (indexOf === -1) {
122+
setTags([...selectedTags, value]);
123+
}
124+
} else if (indexOf !== -1) {
125+
selectedTags.splice(indexOf, 1);
126+
setTags([...selectedTags]);
127+
}
128+
};
129+
130+
/**
131+
* Updates the user's selection for the current question
132+
* and moves to the next one or the final step.
133+
*/
134+
const handleSubmit = (evt) => {
135+
evt.preventDefault();
136+
137+
if (remainingQuestions.length > 0) {
138+
setQuestion(remainingQuestions[0]);
139+
setRemainingQuestions(remainingQuestions.slice(1));
140+
} else {
141+
done(selectedTags);
142+
}
143+
};
144+
145+
return (
146+
<form onSubmit={handleSubmit}>
147+
<fieldset id="quiz">
148+
<legend>{question.message}</legend>
149+
{question.choices.map((choice) => {
150+
const value = `${choice.value}-${
151+
question.dealBreaker ? 'deal' : 'noDeal'
152+
}`;
153+
return (
154+
<div key={choice.value}>
155+
<input
156+
type={question.type || 'radio'}
157+
id={choice.value}
158+
name="question"
159+
value={value}
160+
onChange={handleChange}
161+
/>
162+
<label htmlFor={choice.value}>{choice.name}</label>
163+
<br />
164+
</div>
165+
);
166+
})}
167+
<button className="button button--secondary">Next</button>
168+
</fieldset>
169+
</form>
170+
);
171+
};
172+
173+
export default function RecommendationWizard() {
174+
const [status, setState] = useState(State.Questioning);
175+
const [selections, setSelections] = useState([]);
176+
177+
const done = (choices) => {
178+
setSelections(choices);
179+
setState(State.Ended);
180+
};
181+
182+
const restart = () => {
183+
setState(State.Questioning);
184+
};
185+
186+
let section;
187+
if (status === State.Waiting) {
188+
section = <Intro setState={setState} />;
189+
} else if (status === State.Questioning) {
190+
section = <Questioning questions={questions} done={done} />;
191+
} else if (status === State.Ended) {
192+
section = <FinalRecommendation restart={restart} selections={selections} />;
193+
}
194+
195+
return <article>{section}</article>;
196+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
article {
2+
display: flex;
3+
flex-direction: row;
4+
justify-content: center;
5+
margin: 5em;
6+
}
7+
8+
legend {
9+
background-color: #000;
10+
color: #fff;
11+
padding: 3px 6px;
12+
}
13+
14+
button {
15+
margin: 0.5em 0;
16+
}
17+
18+
li {
19+
list-style: none;
20+
}

src/data/questions.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Uses inquirer format (https://www.npmjs.com/package/inquirer#questions)
2+
export const questions = [
3+
{
4+
category: 'platformSupport',
5+
message:
6+
'Does your application need to run on any mobile platform? (Select all that apply)',
7+
type: 'checkbox',
8+
dealBreaker: true,
9+
choices: [
10+
{ name: 'Android', value: 'android' },
11+
{ name: 'iOS', value: 'ios' },
12+
{ name: 'None', value: 'none' },
13+
],
14+
},
15+
{
16+
category: 'platformSupport',
17+
message:
18+
'Does your application need to run on any desktop platform? (Select all that apply)',
19+
type: 'checkbox',
20+
dealBreaker: true,
21+
choices: [
22+
{ name: 'Linux', value: 'linux' },
23+
{ name: 'macOS', value: 'macos' },
24+
{ name: 'Windows', value: 'windows' },
25+
{ name: 'None', value: 'none' },
26+
],
27+
},
28+
{
29+
category: 'visual',
30+
message:
31+
'Do you want your application to have a consistent look across platforms or do you want it to look closer to the Operating System?',
32+
choices: [
33+
{ name: 'Consistent accross platforms', value: 'customUI' },
34+
{ name: 'Match the OS look and feel', value: 'platformUI' },
35+
{ name: 'Indifferent', value: 'none' },
36+
],
37+
},
38+
{
39+
category: 'fieldType',
40+
message:
41+
'Are you going to start a full new application or does it have to integrate with an existing one?',
42+
choices: [
43+
{ name: 'New application', value: 'greenfield' },
44+
{ name: 'Existing application', value: 'brownfield' },
45+
],
46+
},
47+
{
48+
category: 'targetAudience',
49+
message: 'Who will be the main user of your application?',
50+
choices: [
51+
{ name: 'Consumers', value: 'consumers' },
52+
{ name: 'Enterprise users', value: 'enterprise' },
53+
],
54+
},
55+
{
56+
category: 'team',
57+
notes: 'This depends mostly on enterprise users',
58+
message:
59+
'Is the application going to have a team working fulltime in the longterm?',
60+
choices: [
61+
{ name: 'Yes', value: 'longterm' },
62+
{ name: 'No', value: 'shortterm' },
63+
],
64+
},
65+
{
66+
category: 'visual',
67+
message:
68+
"How visually complex or interactions is going to have your app's main view/page?",
69+
choices: [
70+
{ name: 'Simple layout or interactions', value: 'simpleLayout' },
71+
{ name: 'Definitely not simple', value: 'complexLayout' },
72+
],
73+
},
74+
{
75+
category: 'support',
76+
message:
77+
'Do you think you will need to pay for support or would be help from the community be enough?',
78+
choices: [
79+
{ name: 'Paid support', value: 'paidSupport' },
80+
{ name: 'Community', value: 'community' },
81+
],
82+
},
83+
];

0 commit comments

Comments
 (0)