├── src ├── hooks │ ├── index.js │ └── useNavigation.js ├── components │ ├── index.js │ ├── Header │ │ ├── Header.js │ │ └── Header.module.css │ ├── Input │ │ ├── Input.js │ │ └── Input.module.css │ ├── Todos │ │ ├── ToDos.js │ │ └── ToDos.module.css │ └── Softkey │ │ ├── Softkey.module.css │ │ └── Softkey.js ├── index.js ├── index.css └── App.js ├── docs ├── to-do.png ├── to-do-landscape.gif ├── to-do-on-input.png └── to-do-portrait.gif ├── .vscode ├── settings.json └── tasks.json ├── public ├── assets │ └── icons │ │ ├── kaios_56.png │ │ └── kaios_112.png ├── index.html └── manifest.webapp ├── .gitignore ├── package.json └── README.md /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export * from './useNavigation'; -------------------------------------------------------------------------------- /docs/to-do.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.webapp": "json", 4 | }, 5 | } -------------------------------------------------------------------------------- /docs/to-do-landscape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do-landscape.gif -------------------------------------------------------------------------------- /docs/to-do-on-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do-on-input.png -------------------------------------------------------------------------------- /docs/to-do-portrait.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/docs/to-do-portrait.gif -------------------------------------------------------------------------------- /public/assets/icons/kaios_56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/public/assets/icons/kaios_56.png -------------------------------------------------------------------------------- /public/assets/icons/kaios_112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaiostech/sample-react/HEAD/public/assets/icons/kaios_112.png -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './Input/Input'; 2 | export * from './Header/Header'; 3 | export * from './Todos/ToDos'; 4 | export * from './Softkey/Softkey'; 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import css from './Header.module.css'; 3 | 4 | export const Header = ({ title }) => { 5 | return ( 6 |
7 | {title} 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body, 7 | #root { 8 | font-family: "Open Sans", sans-serif; 9 | margin: 0; 10 | display: flex; 11 | flex-direction: column; 12 | overflow: hidden; 13 | background-color: #E1E2E1 !important; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Input/Input.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import css from "./Input.module.css"; 3 | 4 | export const Input = ({ label, type }) => ( 5 |
6 | 7 | 8 |
9 | ) 10 | -------------------------------------------------------------------------------- /src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | header { 2 | height: 36px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: flex-start; 6 | background-color: #9b27af; 7 | padding: 3px 10px; 8 | } 9 | 10 | header span { 11 | font-size: 16px; 12 | font-weight: 600; 13 | color: #ffffff; 14 | text-transform: uppercase; 15 | } 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | My first React App KaiOS 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea 26 | -------------------------------------------------------------------------------- /src/components/Todos/ToDos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import css from './ToDos.module.css'; 3 | 4 | export const ToDos = ({ toDos }) => { 5 | if (toDos === undefined || !toDos.length) return null; 6 | 7 | return ( 8 |
9 | {toDos.map((toDo, index) => ( 10 | 14 | {toDo.name} 15 | 16 | ))} 17 |
18 | ) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/components/Todos/ToDos.module.css: -------------------------------------------------------------------------------- 1 | .todos { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 0 10px; 5 | } 6 | 7 | .todos span { 8 | display: flex; 9 | align-items: center; 10 | width: 100%; 11 | height: 40px; 12 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, .2); 13 | border-radius: 6px; 14 | padding: 0 10px; 15 | margin-bottom: 9px; 16 | background-color: #F5F5F6; 17 | } 18 | 19 | .todos span[nav-selected="true"] { 20 | background-image: linear-gradient(255deg, #9B27AF, #9B27AF); 21 | color: #FFF; 22 | } 23 | 24 | .todos .completed { 25 | text-decoration: line-through; 26 | } -------------------------------------------------------------------------------- /public/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "My first React App for KaiOS", 4 | "description": "Simple example of a to-do list", 5 | "type": "web", 6 | "launch_path": "/index.html", 7 | "icons": { 8 | "56": "/assets/icons/kaios_56.png", 9 | "112": "/assets/icons/kaios_112.png" 10 | }, 11 | "developer": { 12 | "name": "KaiOS Team", 13 | "url": "https://www.kaiostech.com" 14 | }, 15 | "locales": { 16 | "en-US": { 17 | "name": "My first React App for KaiOS", 18 | "subtitle": "Simple example of a to-do list", 19 | "description": "Simple example of a to-do list" 20 | } 21 | }, 22 | "default_locale": "en-US" 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-first-react-app-kaios", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-scripts": "3.0.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample React app for KaiOS 2 | 3 | Simple example of a to-do list, for more information see [KaiOS Developer Portal](https://developer.kaiostech.com/getting-started/build-your-first-app/sample-code#react) 4 | 5 | ![](./docs/to-do-on-input.png) 6 | ![](./docs/to-do.png) 7 | 8 | In portrait devices 9 | 10 | ![](./docs/to-do-portrait.gif) 11 | 12 | In landscape devices 13 | 14 | ![](./docs/to-do-landscape.gif) 15 | 16 | ## Start 17 | 18 | ```console 19 | npm run start 20 | # or 21 | yarn start 22 | ``` 23 | 24 | ## Build app 25 | 26 | ```console 27 | npm run build 28 | # or 29 | yarn build 30 | ``` 31 | 32 | ## Send the app to a KaiOS device 33 | 34 | follow [os-env-setup](https://developer.kaiostech.com/getting-started/env-setup/os-env-setup) and [test-your-apps](https://developer.kaiostech.com/getting-started/build-your-first-package-app/test-your-apps) 35 | install to your device. 36 | -------------------------------------------------------------------------------- /src/components/Softkey/Softkey.module.css: -------------------------------------------------------------------------------- 1 | .softkey { 2 | height: 30px; 3 | max-height: 30px; 4 | width: 100%; 5 | max-width: 100%; 6 | background: white; 7 | border-top: 2px #cbcbcb solid; 8 | display: flex; 9 | flex-shrink: 0; 10 | white-space: nowrap; 11 | padding: 0 5px; 12 | font-weight: 700; 13 | box-sizing: border-box; 14 | line-height: 26px; 15 | margin-top: auto; 16 | position: absolute; 17 | bottom: 0; 18 | } 19 | 20 | .left, 21 | .right { 22 | font-weight: 600; 23 | font-size: 14px; 24 | color: #242424; 25 | overflow: hidden; 26 | width: 100%; 27 | letter-spacing: -0.5px; 28 | box-sizing: border-box; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .left { 33 | text-align: left; 34 | padding-right: 5px; 35 | } 36 | 37 | .center { 38 | color: #242424; 39 | text-transform: uppercase; 40 | font-size: 18px; 41 | text-align: center; 42 | max-width: 120px; 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | width: 100%; 46 | } 47 | 48 | .right { 49 | text-align: right; 50 | padding-left: 5px; 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Softkey/Softkey.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import css from "./Softkey.module.css"; 3 | 4 | export const Softkey = ({ 5 | left, 6 | onKeyLeft, 7 | center, 8 | onKeyCenter, 9 | right, 10 | onKeyRight 11 | }) => { 12 | useEffect(() => { 13 | document.addEventListener("keydown", handleKeyDown); 14 | 15 | return () => document.removeEventListener("keydown", handleKeyDown); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | 19 | const handleKeyDown = evt => { 20 | switch (evt.key) { 21 | case "SoftLeft": 22 | return onKeyLeft && onKeyLeft(evt); 23 | case "Enter": 24 | return onKeyCenter && onKeyCenter(evt); 25 | case "SoftRight": 26 | return onKeyRight && onKeyRight(evt); 27 | default: 28 | return; 29 | } 30 | }; 31 | 32 | return ( 33 |
34 | 35 | 36 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 5px 10px; 5 | position: relative; 6 | padding-top: 7px; 7 | } 8 | 9 | .input input { 10 | font-family: inherit; 11 | width: 100%; 12 | border: 0; 13 | border-bottom: 1px solid #d2d2d2; 14 | outline: 0; 15 | font-size: 16px; 16 | color: transparent; 17 | padding: 7px 0; 18 | background: transparent; 19 | transition: border-color 0.2s; 20 | } 21 | 22 | .input label { 23 | position: absolute; 24 | bottom: 0; 25 | color: #9b9b9b; 26 | } 27 | 28 | .input input::placeholder { 29 | color: transparent; 30 | } 31 | 32 | .input input:placeholder-shown ~ label { 33 | font-size: 16px; 34 | cursor: text; 35 | top: 20px; 36 | } 37 | 38 | .input input[nav-selected="true"] ~ label { 39 | position: absolute; 40 | top: 0; 41 | display: block; 42 | transition: 0.2s; 43 | font-size: 12px; 44 | color: #9b9b9b; 45 | } 46 | 47 | .input input[nav-selected="true"] ~ label { 48 | color: #9b27af; 49 | } 50 | 51 | .input input[nav-selected="true"] { 52 | padding-bottom: 6px; 53 | border-bottom: 2px solid #9b27af; 54 | text-shadow: 0 0 0 rgba(0, 0, 0, 1); 55 | } 56 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Developer mode (Watch)", 6 | "type": "npm", 7 | "script": "start", 8 | "problemMatcher": [], 9 | "presentation": { 10 | "reveal": "silent", 11 | "panel": "dedicated" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "label": "Build app", 20 | "type": "npm", 21 | "script": "build", 22 | "problemMatcher": [], 23 | "presentation": { 24 | "reveal": "silent", 25 | "panel": "dedicated" 26 | } 27 | }, 28 | { 29 | "label": "Install app", 30 | "type": "npm", 31 | "script": "app:install", 32 | "problemMatcher": [], 33 | "presentation": { 34 | "reveal": "silent", 35 | "panel": "dedicated" 36 | } 37 | }, 38 | { 39 | "label": "Uninstall app", 40 | "type": "npm", 41 | "script": "app:uninstall", 42 | "problemMatcher": [], 43 | "presentation": { 44 | "reveal": "silent", 45 | "panel": "dedicated" 46 | } 47 | }, 48 | { 49 | "label": "Update app", 50 | "type": "npm", 51 | "script": "app:update", 52 | "problemMatcher": [], 53 | "presentation": { 54 | "reveal": "silent", 55 | "panel": "dedicated" 56 | } 57 | }, 58 | { 59 | "label": "Start app", 60 | "type": "npm", 61 | "script": "app:start", 62 | "problemMatcher": [], 63 | "presentation": { 64 | "reveal": "silent", 65 | "panel": "dedicated" 66 | } 67 | }, 68 | { 69 | "label": "Stop app", 70 | "type": "npm", 71 | "script": "app:stop", 72 | "problemMatcher": [], 73 | "presentation": { 74 | "reveal": "silent", 75 | "panel": "dedicated" 76 | } 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Header, Input, ToDos, Softkey } from "./components"; 3 | import { useNavigation } from "./hooks"; 4 | 5 | export default function App() { 6 | const [toDos, setToDo] = useState([]); 7 | 8 | const [current, setNavigation] = useNavigation(); 9 | 10 | const onKeyCenter = () => { 11 | const currentElement = document.querySelector("[nav-selected=true]"); 12 | const currentNavigationIndex = parseInt(currentElement.getAttribute("nav-index"), 10); 13 | 14 | const isATask = currentNavigationIndex > 0; 15 | if (isATask) { 16 | setToDo(prevState => { 17 | const current = [...prevState]; 18 | current[currentNavigationIndex - 1].completed = !current[currentNavigationIndex - 1].completed; 19 | return current; 20 | }); 21 | } else if (currentElement.value.length) { 22 | const toDo = { name: currentElement.value, completed: false }; 23 | setToDo(prevState => [...prevState, toDo]); 24 | currentElement.value = ""; 25 | } 26 | }; 27 | 28 | const onKeyRight = () => { 29 | const currentIndex = parseInt( 30 | document.querySelector("[nav-selected=true]").getAttribute("nav-index"), 31 | 10 32 | ); 33 | if (currentIndex > 0) { 34 | setToDo(prevState => { 35 | const current = [...prevState]; 36 | current.splice(currentIndex - 1, 1); 37 | const goToPreviousElement = Boolean(current.length); 38 | setNavigation(goToPreviousElement ? currentIndex - 1 : 0); 39 | return current; 40 | }); 41 | } 42 | }; 43 | 44 | return ( 45 | <> 46 |
47 | 48 | 49 | 50 | 51 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/hooks/useNavigation.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export const useNavigation = () => { 4 | useEffect(() => { 5 | document.addEventListener('keydown', onKeyDown); 6 | setNavigation(0); 7 | 8 | return () => document.removeEventListener('keydown', onKeyDown); 9 | // eslint-disable-next-line react-hooks/exhaustive-deps 10 | }, []); 11 | 12 | const [current, setCurrent] = useState({ type: null, index: null }); 13 | 14 | const getAllElements = () => document.querySelectorAll('[nav-selectable]'); 15 | 16 | const getTheIndexOfTheSelectedElement = () => { 17 | const element = document.querySelector('[nav-selected=true]'); 18 | return element ? parseInt(element.getAttribute('nav-index')) : 0; 19 | } 20 | 21 | const setNavigation = index => selectElement(getAllElements()[index] || document.body); 22 | 23 | const onKeyDown = evt => { 24 | if (evt.key !== 'ArrowDown' && evt.key !== 'ArrowUp') return; 25 | 26 | const allElements = getAllElements(); 27 | const currentIndex = getTheIndexOfTheSelectedElement(); 28 | 29 | let setIndex; 30 | switch (evt.key) { 31 | case 'ArrowDown': 32 | const goToFirstElement = currentIndex + 1 > allElements.length - 1; 33 | setIndex = goToFirstElement ? 0 : currentIndex + 1; 34 | return selectElement(allElements[setIndex] || allElements[0], setIndex); 35 | case 'ArrowUp': 36 | const goToLastElement = currentIndex === 0; 37 | setIndex = goToLastElement ? allElements.length - 1 : currentIndex - 1; 38 | return selectElement(allElements[setIndex] || allElements[0], setIndex); 39 | default: 40 | break; 41 | } 42 | } 43 | 44 | const selectElement = (selectElement, setIndex = 0) => { 45 | if (selectElement) { 46 | [].forEach.call(getAllElements(), (element, index) => { 47 | const selectThisElement = element; 48 | element.setAttribute("nav-selected", element === selectElement); 49 | element.setAttribute("nav-index", index); 50 | if (selectThisElement) { 51 | selectThisElement.scrollIntoView(true); 52 | if (element.nodeName === 'INPUT') { 53 | element.focus(); 54 | } else { 55 | element.blur(); 56 | } 57 | } 58 | }); 59 | setCurrent({ type: selectElement.tagName, index: setIndex }); 60 | } else { 61 | setNavigation(0); 62 | } 63 | } 64 | 65 | return [current, setNavigation]; 66 | }; 67 | --------------------------------------------------------------------------------