diff --git a/README.md b/README.md index 3c5b4a6..ac67598 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,4 @@ The experiments are currently organized based on the language they use: - [Idris](./idris/) - [Typst](./typst/) - [Python](./python/) +- [Elm](./elm/) diff --git a/elm/README.md b/elm/README.md new file mode 100644 index 0000000..558568d --- /dev/null +++ b/elm/README.md @@ -0,0 +1,5 @@ +# Elm + +| Name | Description | +| ----------------------- | ----------------------------- | +| [todolist](./todolist/) | Basic todolist implementation | diff --git a/elm/todolist/.github/workflows/elm.yml b/elm/todolist/.github/workflows/elm.yml new file mode 100644 index 0000000..ad95080 --- /dev/null +++ b/elm/todolist/.github/workflows/elm.yml @@ -0,0 +1,29 @@ +name: Elm + +on: [push] + +jobs: + deploy: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [12.x] + + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Use node 12 + uses: actions/setup-node@v1 + + with: + node-version: ${{ matrix.node-version }} + - run: npm i -g elm create-elm-app + - run: elm-app build + - name: Deploy to github pages + uses: JamesIves/github-pages-deploy-action@master + env: + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + BASE_BRANCH: master + BRANCH: gh-pages + FOLDER: build diff --git a/elm/todolist/.gitignore b/elm/todolist/.gitignore new file mode 100644 index 0000000..e7e7f12 --- /dev/null +++ b/elm/todolist/.gitignore @@ -0,0 +1,14 @@ +# Distribution +build/ + +# elm-package generated files +elm-stuff + +# elm-repl generated files +repl-temp-* + +# Dependency directories +node_modules + +# Desktop Services Store on macOS +.DS_Store diff --git a/elm/todolist/README.md b/elm/todolist/README.md new file mode 100644 index 0000000..3b963fc --- /dev/null +++ b/elm/todolist/README.md @@ -0,0 +1,3 @@ +# Elm todo app + +Todo app made in 1h for getting a better understanding of the elm architecture diff --git a/elm/todolist/elm-analyse.json b/elm/todolist/elm-analyse.json new file mode 100644 index 0000000..2e716e1 --- /dev/null +++ b/elm/todolist/elm-analyse.json @@ -0,0 +1,6 @@ +{ + "checks": { + "ExposeAll": false, + "ImportAll": false + } +} diff --git a/elm/todolist/elm.json b/elm/todolist/elm.json new file mode 100644 index 0000000..5e90087 --- /dev/null +++ b/elm/todolist/elm.json @@ -0,0 +1,26 @@ +{ + "type": "application", + "source-directories": ["src"], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.1", + "elm/core": "1.0.2", + "elm/html": "1.0.0" + }, + "indirect": { + "elm/json": "1.1.2", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2" + } + }, + "test-dependencies": { + "direct": { + "elm-explorations/test": "1.0.0" + }, + "indirect": { + "elm/random": "1.0.0" + } + } +} diff --git a/elm/todolist/elmapp.config.js b/elm/todolist/elmapp.config.js new file mode 100644 index 0000000..ae3810d --- /dev/null +++ b/elm/todolist/elmapp.config.js @@ -0,0 +1,3 @@ +module.exports = { + homepage: "https://mateadrielrafael.github.io/elmTodolist" +}; diff --git a/elm/todolist/public/favicon.ico b/elm/todolist/public/favicon.ico new file mode 100644 index 0000000..d7057bd Binary files /dev/null and b/elm/todolist/public/favicon.ico differ diff --git a/elm/todolist/public/index.html b/elm/todolist/public/index.html new file mode 100644 index 0000000..7bef8ed --- /dev/null +++ b/elm/todolist/public/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + Elm App + + + +
+ + diff --git a/elm/todolist/public/logo.svg b/elm/todolist/public/logo.svg new file mode 100644 index 0000000..4321d4d --- /dev/null +++ b/elm/todolist/public/logo.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/elm/todolist/public/manifest.json b/elm/todolist/public/manifest.json new file mode 100644 index 0000000..9b7dc41 --- /dev/null +++ b/elm/todolist/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Elm App", + "name": "Create Elm App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/elm/todolist/src/Main.elm b/elm/todolist/src/Main.elm new file mode 100644 index 0000000..8413e38 --- /dev/null +++ b/elm/todolist/src/Main.elm @@ -0,0 +1,167 @@ +module Main exposing (..) + +import Browser +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) + + + +-- HELPER TYPES + + +type alias Todo = + { done : Bool + , text : String + , id : Int + } + + + +---- MODEL ---- + + +type alias Model = + { todos : List Todo + , lastId : Int + , nextTodoText : String + } + + +init : Model +init = + Model [] 0 "" + + + +---- UPDATE ---- + + +type Msg + = AddTodo + | DeleteTodo Int + | ToggleTodoCompletion Int + | SetNextTodoText String + + +update : Msg -> Model -> Model +update message model = + case message of + AddTodo -> + if model.nextTodoText == "" then + model + + else + let + id = + model.lastId + 1 + + text = + model.nextTodoText + in + { model | lastId = id, todos = Todo False text id :: model.todos } + + DeleteTodo id -> + { model | todos = List.filter ((/=) id << .id) model.todos } + + ToggleTodoCompletion id -> + { model + | todos = + List.map + (\todo -> + if todo.id == id then + { todo | done = not todo.done } + + else + todo + ) + model.todos + } + + SetNextTodoText text -> + { model | nextTodoText = text } + + + +---- VIEW ---- + + +view : Model -> Html Msg +view model = + div [ class "container" ] + [ header model + , div [ class "todos" ] <| + List.map + todoView + model.todos + ] + + + +-- HEADER + + +header : Model -> Html Msg +header _ = + div [ class "header" ] + [ button + [ class "btn" + , onClick AddTodo + ] + [ text "add todo" ] + , input + [ placeholder "todo text" + , onInput SetNextTodoText + , class "todoTextInput" + ] + [] + ] + + + +-- _TODO VIEW + + +todoClasses : Todo -> String +todoClasses todo = + "todo" + ++ (if todo.done then + " completed" + + else + "" + ) + + +todoView : Todo -> Html Msg +todoView todo = + div [ class <| todoClasses todo ] + [ div [ class "todoCompleted" ] + [ input + [ checked todo.done + , type_ "checkbox" + , class "todoCheckbox" + , onCheck <| \_ -> ToggleTodoCompletion todo.id + ] + [] + ] + , div [ class "todoText" ] [ text todo.text ] + , button + [ class "btn" + , onClick <| DeleteTodo todo.id + ] + [ text "Delete todo" ] + ] + + + +---- PROGRAM ---- + + +main : Program () Model Msg +main = + Browser.sandbox + { view = view + , init = init + , update = update + } diff --git a/elm/todolist/src/index.js b/elm/todolist/src/index.js new file mode 100644 index 0000000..c9bbd20 --- /dev/null +++ b/elm/todolist/src/index.js @@ -0,0 +1,12 @@ +import './main.css'; +import { Elm } from './Main.elm'; +import * as serviceWorker from './serviceWorker'; + +Elm.Main.init({ + node: document.getElementById('root') +}); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/elm/todolist/src/main.css b/elm/todolist/src/main.css new file mode 100644 index 0000000..206b7b0 --- /dev/null +++ b/elm/todolist/src/main.css @@ -0,0 +1,111 @@ +/* + elm-hot creates an additional div wrapper around the app to make HMR possible. + This could break styling in development mode if you are using Elm UI. + + More context in the issue: + https://github.com/halfzebra/create-elm-app/issues/320 +*/ +[data-elm-hot="true"] { + height: inherit; +} + +:root { + --secondary: #009fb7; + --primary: #1a213b; + --on-secondary: #eff1f4; + --disabled: #555555; +} + +body { + font-family: "Source Sans Pro", "Trebuchet MS", "Lucida Grande", + "Bitstream Vera Sans", "Helvetica Neue", sans-serif; + margin: 0; + background: var(--primary); + + color: var(--on-secondary); +} + +h1 { + font-size: 30px; +} + +.header { + display: flex; +} + +.btn { + background-color: var(--secondary); + color: var(--on-secondary); + padding: 1rem; + margin: 1rem; + border: none; +} + +input { + outline: none; +} + +.todos { + display: flex; + flex-direction: column; + min-height: 67vh; + border: 1px solid var(--secondary); +} + +.todo { + transition: filter 0.5s; + background: var(--primary); + display: flex; +} + +.todo:hover { + filter: brightness(1.4); +} + +.todo.completed { + color: var(--disabled); + + text-decoration: line-through; +} + +.todo.completed > .btn { + color: var(--disabled); +} + +.todo > * { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.todoText { + justify-content: flex-start; + flex-direction: row; + flex-grow: 1; +} + +.todoCompleted { + margin: 1rem; +} + +.todoTextInput { + background: var(--primary); + border: 1px var(--secondary) solid; + margin: 1rem; + padding: 2px; + box-sizing: border-box; + color: var(--on-secondary); + font-size: 1.5rem; + transition: filter 0.5s; +} + +.todoTextInput:focus { + filter: brightness(1.4); +} + +.todoCheckbox { + width: 1rem; + height: 1rem; + background: var(--secondary); +} diff --git a/elm/todolist/src/serviceWorker.js b/elm/todolist/src/serviceWorker.js new file mode 100644 index 0000000..f8c7e50 --- /dev/null +++ b/elm/todolist/src/serviceWorker.js @@ -0,0 +1,135 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +}