From 6c923b1bbb9877fe9e27691724bb63afa5369a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3n=20Molleda?= Date: Fri, 24 Sep 2021 10:37:46 -0700 Subject: [PATCH] feat: recommendation engine infrastructure --- docusaurus.config.js | 5 + src/components/RecommendationWizard.js | 208 ++++++++++++++++++ .../RecommendationWizard.module.css | 20 ++ src/data/questions.js | 83 +++++++ src/data/technologies.js | 91 ++++++++ src/pages/recommendation.js | 38 ++++ 6 files changed, 445 insertions(+) create mode 100644 src/components/RecommendationWizard.js create mode 100644 src/components/RecommendationWizard.module.css create mode 100644 src/data/questions.js create mode 100644 src/data/technologies.js create mode 100644 src/pages/recommendation.js diff --git a/docusaurus.config.js b/docusaurus.config.js index 5983d62..3b29d05 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -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', diff --git a/src/components/RecommendationWizard.js b/src/components/RecommendationWizard.js new file mode 100644 index 0000000..2a2e415 --- /dev/null +++ b/src/components/RecommendationWizard.js @@ -0,0 +1,208 @@ +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); + if (technologies.length === 0) { + return ( +
+

+ We could not find any technology that checks all your criteria. Please + try again changing some of the values (like the targetted platforms). +

+ +
+ ); + } + return ( +
+

+ Based on your answers the technologies we think you should investigate + are: +

+ + + +

+ Doesn't seem right? Open an{' '} + + issue + {' '} + with more details! +

+
+ ); +}; + +/** + * + * @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 ( +
+
+ {question.message} + {question.choices.map((choice) => { + const value = `${choice.value}-${ + question.dealBreaker ? 'deal' : 'noDeal' + }`; + return ( +
+ + +
+
+ ); + })} + +
+
+ ); +}; + +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 = ; + } else if (status === State.Questioning) { + section = ; + } else if (status === State.Ended) { + section = ; + } + + return
{section}
; +} diff --git a/src/components/RecommendationWizard.module.css b/src/components/RecommendationWizard.module.css new file mode 100644 index 0000000..8c13db9 --- /dev/null +++ b/src/components/RecommendationWizard.module.css @@ -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; +} \ No newline at end of file diff --git a/src/data/questions.js b/src/data/questions.js new file mode 100644 index 0000000..dabb5c5 --- /dev/null +++ b/src/data/questions.js @@ -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' }, + ], + }, + ]; diff --git a/src/data/technologies.js b/src/data/technologies.js new file mode 100644 index 0000000..0842de6 --- /dev/null +++ b/src/data/technologies.js @@ -0,0 +1,91 @@ +// TODO: This information should come from /data/technologies/${technology}.json directly instead of being handcrafted +export const technologies = [ + { + name: 'Electron', + normalizedName: 'electron', + categories: { + linux: 5, + windows: 5, + macos: 5, + android: 0, + ios: 0, + devEx: 3, + community: 3, + customUI: 5, + greenfield: 5, + brownfield: 2, + complexLayout: 4, + }, + }, + { + name: 'PWA', + normalizedName: 'pwa', + categories: { + linux: 5, + windows: 5, + macos: 5, + android: 4, + ios: 2, + devEx: 2, + community: 4, + customUI: 5, + platformUI: 0, + greenfield: 5, + complexLayout: 4, + }, + }, + { + name: 'Xamarin', + normalizedName: 'xamarin', + categories: { + linux: 0, + windows: 5, + macos: 4, + android: 3, + ios: 3, + devEx: 4, + paidSupport: 4, + community: 3, + customUI: 2, + platformUI: 4, + greenfield: 5, + complexLayout: 3, + }, + }, + { + name: 'WebView2', + normalizedName: 'webview2', + categories: { + linux: 0, + windows: 4, + macos: 0, + android: 0, + ios: 0, + devEx: 1, + paidSupport: 3, + community: 1, + customUI: 5, + greenfield: 5, + brownfield: 2, + complexLayout: 5, + }, + }, + { + name: 'React Native', + normalizedName: 'react-native', + categories: { + linux: 0, + windows: 3, + macos: 2, + android: 5, + ios: 5, + devEx: 3, + community: 4, + customUI: 2, + platformUI: 5, + greenfield: 5, + brownfield: 3, + complexLayout: 3, + }, + }, + ]; diff --git a/src/pages/recommendation.js b/src/pages/recommendation.js new file mode 100644 index 0000000..a887efb --- /dev/null +++ b/src/pages/recommendation.js @@ -0,0 +1,38 @@ +import React from 'react'; +import clsx from 'clsx'; +import Layout from '@theme/Layout'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import styles from './index.module.css'; + +import RecommendationWizard from '../components/RecommendationWizard'; + +function RecommendationHeader({ title, description }) { + const { siteConfig } = useDocusaurusContext(); + return ( +
+
+

{title}

+

{description}

+
+
+ ); +} + +export default function Recommendation() { + const { siteConfig } = useDocusaurusContext(); + const title = `Get recommendations for your next cross-platform project`; + const description = `Site still in beta!`; + return ( + + +
+ +
+
+ ); +}