Skip to content

Commit 6c923b1

Browse files
committed
feat: recommendation engine infrastructure
1 parent 5505f24 commit 6c923b1

File tree

6 files changed

+445
-0
lines changed

6 files changed

+445
-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: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
if (technologies.length === 0) {
65+
return (
66+
<div>
67+
<p>
68+
We could not find any technology that checks all your criteria. Please
69+
try again changing some of the values (like the targetted platforms).
70+
</p>
71+
<button onClick={restart} className="button button--secondary">
72+
Start again!
73+
</button>
74+
</div>
75+
);
76+
}
77+
return (
78+
<div>
79+
<p>
80+
Based on your answers the technologies we think you should investigate
81+
are:
82+
</p>
83+
<ul>
84+
{technologies.map((technology) => {
85+
return (
86+
<li>
87+
<a href={`/docs/${technology.normalizedName}`}>
88+
{technology.name}
89+
</a>
90+
</li>
91+
);
92+
})}
93+
</ul>
94+
<button onClick={restart} className="button button--secondary">
95+
Start again!
96+
</button>
97+
98+
<p>
99+
Doesn't seem right? Open an{' '}
100+
<a href="https://github.com/crossplatform-dev/crossplatform.dev/issues/new">
101+
issue
102+
</a>{' '}
103+
with more details!
104+
</p>
105+
</div>
106+
);
107+
};
108+
109+
/**
110+
*
111+
* @param {QuestioningProps} param0
112+
* @returns
113+
*/
114+
const Questioning = ({ questions, done }) => {
115+
const [question, setQuestion] = useState(questions[0]);
116+
const [remainingQuestions, setRemainingQuestions] = useState(
117+
questions.slice(1)
118+
);
119+
const [selectedTags, setTags] = useState([]);
120+
121+
/**
122+
* Handles the selection changes of inputs in the form to make
123+
* sure their state is updated in the React side.
124+
*/
125+
const handleChange = (e) => {
126+
const { checked, value } = e.target;
127+
if (value === 'none') {
128+
return;
129+
}
130+
const indexOf = selectedTags.indexOf(value);
131+
132+
if (checked) {
133+
if (indexOf === -1) {
134+
setTags([...selectedTags, value]);
135+
}
136+
} else if (indexOf !== -1) {
137+
selectedTags.splice(indexOf, 1);
138+
setTags([...selectedTags]);
139+
}
140+
};
141+
142+
/**
143+
* Updates the user's selection for the current question
144+
* and moves to the next one or the final step.
145+
*/
146+
const handleSubmit = (evt) => {
147+
evt.preventDefault();
148+
149+
if (remainingQuestions.length > 0) {
150+
setQuestion(remainingQuestions[0]);
151+
setRemainingQuestions(remainingQuestions.slice(1));
152+
} else {
153+
done(selectedTags);
154+
}
155+
};
156+
157+
return (
158+
<form onSubmit={handleSubmit}>
159+
<fieldset id="quiz">
160+
<legend>{question.message}</legend>
161+
{question.choices.map((choice) => {
162+
const value = `${choice.value}-${
163+
question.dealBreaker ? 'deal' : 'noDeal'
164+
}`;
165+
return (
166+
<div key={choice.value}>
167+
<input
168+
type={question.type || 'radio'}
169+
id={choice.value}
170+
name="question"
171+
value={value}
172+
onChange={handleChange}
173+
/>
174+
<label htmlFor={choice.value}>{choice.name}</label>
175+
<br />
176+
</div>
177+
);
178+
})}
179+
<button className="button button--secondary">Next</button>
180+
</fieldset>
181+
</form>
182+
);
183+
};
184+
185+
export default function RecommendationWizard() {
186+
const [status, setState] = useState(State.Questioning);
187+
const [selections, setSelections] = useState([]);
188+
189+
const done = (choices) => {
190+
setSelections(choices);
191+
setState(State.Ended);
192+
};
193+
194+
const restart = () => {
195+
setState(State.Questioning);
196+
};
197+
198+
let section;
199+
if (status === State.Waiting) {
200+
section = <Intro setState={setState} />;
201+
} else if (status === State.Questioning) {
202+
section = <Questioning questions={questions} done={done} />;
203+
} else if (status === State.Ended) {
204+
section = <FinalRecommendation restart={restart} selections={selections} />;
205+
}
206+
207+
return <article>{section}</article>;
208+
}
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)