├── .env.production ├── .github └── workflows │ └── deploy-to-packages.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build ├── asset-manifest.json ├── ban ├── favicon.ico ├── images │ ├── logo-192.png │ ├── logo-310.png │ └── logo-512.png ├── index.html ├── login.html ├── logo.gif ├── logo.html ├── logo.min.gif ├── logo.min.html ├── logo.min.svg ├── logo.png ├── logo.svg ├── manifest.json ├── precache-manifest.c7e25e80dac3bf390c6499d7c9354ba4.js ├── robots.txt ├── service-worker.js └── static │ ├── css │ └── main.3953ab90.chunk.css │ ├── js │ ├── main.66bf26de.chunk.js │ ├── runtime~main.1b922744.js │ └── vendors~main.27ac911d.chunk.js │ └── media │ └── cybericons.92e582d8.svg ├── data_example.json ├── doc └── updateEvents.graphml ├── elm.json ├── icons ├── cybericons.sfd └── fontello-82f9af8f.zip ├── public ├── ban ├── favicon.ico ├── images │ ├── logo-192.png │ ├── logo-310.png │ └── logo-512.png ├── index.html ├── login.html ├── logo.gif ├── logo.html ├── logo.min.gif ├── logo.min.html ├── logo.min.svg ├── logo.png ├── logo.svg ├── manifest.json └── robots.txt ├── src ├── Calendar │ ├── Calendar.elm │ ├── Day.elm │ ├── Event.elm │ ├── Helpers.elm │ ├── JourFerie.elm │ ├── Msg.elm │ └── Week.elm ├── Config.elm ├── Cyberplanning │ ├── Cyberplanning.elm │ ├── PlanningRequest.elm │ ├── Query.elm │ ├── Storage.elm │ ├── Types.elm │ └── Utils.elm ├── Main.elm ├── Model.elm ├── Msg.elm ├── MyTime.elm ├── Personnel │ ├── Ical.elm │ ├── Personnel.elm │ ├── Storage.elm │ ├── Timeparser.elm │ └── Types.elm ├── Secret │ ├── Help.elm │ └── Secret.elm ├── Storage.elm ├── Update.elm ├── Utils.elm ├── Vendor │ ├── Color.elm │ ├── Swipe.elm │ └── TimeZone.elm ├── View │ ├── SideMenu.elm │ ├── Tooltip.elm │ └── View.elm ├── css │ ├── calendar.css │ ├── colors.css │ ├── cybericons.css │ ├── font │ │ ├── cybericons.eot │ │ ├── cybericons.svg │ │ ├── cybericons.ttf │ │ ├── cybericons.woff │ │ └── cybericons.woff2 │ ├── main.css │ ├── material-checkbox.css │ ├── personnel.css │ ├── secret.css │ ├── sidemenu.css │ └── tooltip.css ├── index.js └── service │ └── registerServiceWorker.js └── tests ├── Tests.elm ├── Timeout.elm └── elm-package.json /.env.production: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /.github/workflows/deploy-to-packages.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Create and publish a Docker image v2 7 | 8 | on: 9 | push: 10 | branches: ['release'] 11 | workflow_dispatch: 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | build-and-push-image: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | 28 | - name: Log in to the Container registry 29 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Extract metadata (tags, labels) for Docker 36 | id: meta 37 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 38 | with: 39 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 40 | 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 43 | with: 44 | context: . 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # elm-package generated files 2 | elm-stuff 3 | 4 | # elm-repl generated files 5 | repl-temp-* 6 | 7 | # Dependency directories 8 | node_modules 9 | 10 | # Desktop Services Store on macOS 11 | .DS_Store 12 | 13 | proto/ 14 | 15 | .vscode/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | RUN npm install -g --unsafe-perm create-elm-app 6 | 7 | COPY . . 8 | 9 | RUN /usr/local/bin/elm-app build 10 | 11 | FROM nginx 12 | 13 | COPY --from=builder /app/build /usr/share/nginx/html/ 14 | 15 | EXPOSE 80 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cyberplanning webclient 2 | 3 | Web calendar in [ELM lang](https://elm-lang.org/) 4 | 5 | ## Configure 6 | 7 | Configuration file is in `src/Config.elm` 8 | 9 | ## Compile 10 | 11 | ### Via Docker 12 | 13 | Create the image 14 | 15 | ``` 16 | docker build . --tag elm-compiler:latest 17 | ``` 18 | 19 | To build the application with the docker image: 20 | 21 | ``` 22 | docker run --rm -v $PWD:/app elm-compiler 23 | ``` 24 | 25 | This must be executed in the application folder 26 | 27 | To start in development mode with server and hot reload: 28 | 29 | ``` 30 | docker run --rm -it -p 3000:3000 -v $PWD:/app elm-compiler start 31 | ``` 32 | 33 | ### Via cli 34 | 35 | Install `nodejs` using your package manager or [nvm](https://github.com/nvm-sh/nvm) 36 | 37 | Install `elm` and `create-elm-app` using `npm` 38 | 39 | ``` 40 | npm install -g elm 41 | npm install -g create-elm-app 42 | ``` 43 | 44 | Compile using [create-elm-app](https://github.com/halfzebra/create-elm-app) 45 | 46 | ``` 47 | elm-app build 48 | ``` 49 | 50 | To start in development mode with server and hot reload: 51 | 52 | ``` 53 | elm-app start 54 | ``` 55 | 56 | 57 | ## Documentation 58 | 59 | ### Query GraphQL 60 | 61 | [Cyberplanning API](https://github.com/cyberplanning/apiserver) 62 | 63 | ```js 64 | query day_planning($grs: [String], $to: DateTime!, $from: DateTime!, $hack2g2: Boolean!, $custom: Boolean!) { 65 | planning(collection: CYBER, affiliationGroups: $grs, toDate: $to, fromDate: $from) { 66 | ...events 67 | } 68 | hack2g2: planning(collection: HACK2G2, toDate: $to, fromDate: $from) @include(if: $hack2g2) { 69 | ...events 70 | } 71 | custom: planning(collection: CUSTOM, toDate: $to, fromDate: $from) @include(if: $custom) { 72 | ...events 73 | } 74 | } 75 | 76 | fragment events on Planning { 77 | events { 78 | title 79 | eventId 80 | startDate 81 | endDate 82 | classrooms 83 | teachers 84 | groups 85 | } 86 | } 87 | 88 | ``` 89 | 90 | Variables 91 | 92 | ``` 93 | { 94 | "from": "2018-11-12T12:00:00.000", 95 | "to": "2018-12-12T12:00:00.000", 96 | "grs": ["21"], 97 | "hack2g2": true, 98 | "custom": true 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "/static/css/main.3953ab90.chunk.css", 3 | "main.js": "/static/js/main.66bf26de.chunk.js", 4 | "runtime~main.js": "/static/js/runtime~main.1b922744.js", 5 | "vendors~main.js": "/static/js/vendors~main.27ac911d.chunk.js", 6 | "./service-worker.js": "/./service-worker.js", 7 | "ban": "/ban", 8 | "favicon.ico": "/favicon.ico", 9 | "images/logo-192.png": "/images/logo-192.png", 10 | "images/logo-310.png": "/images/logo-310.png", 11 | "images/logo-512.png": "/images/logo-512.png", 12 | "index.html": "/index.html", 13 | "login.html": "/login.html", 14 | "logo.gif": "/logo.gif", 15 | "logo.html": "/logo.html", 16 | "logo.min.gif": "/logo.min.gif", 17 | "logo.min.html": "/logo.min.html", 18 | "logo.min.svg": "/logo.min.svg", 19 | "logo.png": "/logo.png", 20 | "logo.svg": "/logo.svg", 21 | "manifest.json": "/manifest.json", 22 | "precache-manifest.c7e25e80dac3bf390c6499d7c9354ba4.js": "/precache-manifest.c7e25e80dac3bf390c6499d7c9354ba4.js", 23 | "robots.txt": "/robots.txt", 24 | "static/media/cybericons.css": "/static/media/cybericons.92e582d8.svg" 25 | } -------------------------------------------------------------------------------- /build/ban: -------------------------------------------------------------------------------- 1 | .....'',;;::cccllllllllllllcccc:::;;,,,''...'',,'.. 2 | ..';cldkO00KXNNNNXXXKK000OOkkkkkxxxxxddoooddddddxxxxkkkkOO0XXKx:. 3 | .':ok0KXXXNXK0kxolc:;;,,,,,,,,,,,;;,,,''''''',,''.. .'lOXKd' 4 | .,lx00Oxl:,'............''''''................... ...,;;'. .oKXd. 5 | .ckKKkc'...'',:::;,'.........'',;;::::;,'..........'',;;;,'.. .';;'. 'kNKc. 6 | .:kXXk:. .. .................. .............,:c:'...;:'. .dNNx. 7 | :0NKd, .....''',,,,''.. ',...........',,,'',,::,...,,. .dNNx. 8 | .xXd. .:;'.. ..,' .;,. ...,,'';;'. ... .oNNo 9 | .0K. .;. ;' '; .'...'. .oXX: 10 | .oNO. . ,. . ..',::ccc:;,.. .. lXX: 11 | .dNX: ...... ;. 'cxOKK0OXWWWWWWWNX0kc. :KXd. 12 | .l0N0; ;d0KKKKKXK0ko:... .l0X0xc,...lXWWWWWWWWKO0Kx' ,ONKo. 13 | .lKNKl...'......'. .dXWN0kkk0NWWWWWN0o. :KN0;. .,cokXWWNNNNWNKkxONK: .,:c:. .';;;;:lk0XXx; 14 | :KN0l';ll:'. .,:lodxxkO00KXNWWWX000k. oXNx;:okKX0kdl:::;'',;coxkkd, ...'. ...'''.......',:lxKO:. 15 | oNNk,;c,'',. ...;xNNOc,. ,d0X0xc,. .dOd, ..;dOKXK00000Ox:. ..''dKO, 16 | 'KW0,:,.,:..,oxkkkdl;'. 'KK' .. .dXX0o:'....,:oOXNN0d;.'. ..,lOKd. .. ;KXl. 17 | ;XNd,; ;. l00kxoooxKXKx:..ld: ;KK' .:dkO000000Okxl;. c0; :KK; . ;XXc 18 | 'XXdc. :. .. '' 'kNNNKKKk, .,dKNO. .... .'c0NO' :X0. ,. xN0. 19 | .kNOc' ,. .00. ..''... .l0X0d;. 'dOkxo;... .;okKXK0KNXx;. .0X: ,. lNX' 20 | ,KKdl .c, .dNK, .;xXWKc. .;:coOXO,,'....... .,lx0XXOo;...oNWNXKk:.'KX; ' dNX. 21 | :XXkc'.... .dNWXl .';l0NXNKl. ,lxkkkxo' .cK0. ..;lx0XNX0xc. ,0Nx'.','.kXo ., ,KNx. 22 | cXXd,,;:, .oXWNNKo' .'.. .'.'dKk; .cooollox;.xXXl ..,cdOKXXX00NXc. 'oKWK' ;k: .l. ,0Nk. 23 | cXNx. . ,KWX0NNNXOl'. .o0Ooldk; .:c;.':lxOKKK0xo:,.. ;XX: .,lOXWWXd. . .':,.lKXd. 24 | lXNo cXWWWXooNWNXKko;'.. .lk0x; ...,:ldk0KXNNOo:,.. ,OWNOxO0KXXNWNO, ....'l0Xk, 25 | .dNK. oNWWNo.cXK;;oOXNNXK0kxdolllllooooddxk00KKKK0kdoc:c0No .'ckXWWWNXkc,;kNKl. .,kXXk, 26 | 'KXc .dNWWX;.xNk. .kNO::lodxkOXWN0OkxdlcxNKl,.. oN0'..,:ox0XNWWNNWXo. ,ONO' .o0Xk; 27 | .ONo oNWWN0xXWK, .oNKc .ONx. ;X0. .:XNKKNNWWWWNKkl;kNk. .cKXo. .ON0; 28 | .xNd cNWWWWWWWWKOkKNXxl:,'...;0Xo'.....'lXK;...',:lxk0KNWWWWNNKOd:.. lXKclON0: .xNk. 29 | .dXd ;XWWWWWWWWWWWWWWWWWWNNNNNWWNNNNNNNNNWWNNNNNNWWWWWNXKNNk;.. .dNWWXd. cXO. 30 | .xXo .ONWNWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWNNK0ko:'..OXo 'l0NXx, :KK, 31 | .OXc :XNk0NWXKNWWWWWWWWWWWWWWWWWWWWWNNNX00NNx:'.. lXKc. 'lONN0l. .oXK: 32 | .KX; .dNKoON0;lXNkcld0NXo::cd0NNO:;,,'.. .0Xc lXXo..'l0NNKd,. .c0Nk, 33 | :XK. .xNX0NKc.cXXl ;KXl .dN0. .0No .xNXOKNXOo,. .l0Xk;. 34 | .dXk. .lKWN0d::OWK; lXXc .OX: .ONx. . .,cdk0XNXOd;. .'''....;c:'..;xKXx, 35 | .0No .:dOKNNNWNKOxkXWXo:,,;ONk;,,,,,;c0NXOxxkO0XXNXKOdc,. ..;::,...;lol;..:xKXOl. 36 | ,XX: ..';cldxkOO0KKKXXXXXXXXXXKKKKK00Okxdol:;'.. .';::,..':llc,..'lkKXkc. 37 | :NX' . '' .................. .,;:;,',;ccc;'..'lkKX0d;. 38 | lNK. .; ,lc,. ................ ..,,;;;;;;:::,....,lkKX0d:. 39 | .oN0. .'. .;ccc;,'.... ....'',;;;;;;;;;;'.. .;oOXX0d:. 40 | .dN0. .;;,.. .... ..''''''''.... .:dOKKko;. 41 | lNK' ..,;::;;,'......................... .;d0X0kc'. 42 | .xXO' .;oOK0x:. 43 | .cKKo. .,:oxkkkxk0K0xc'. 44 | .oKKkc,. .';cok0XNNNX0Oxoc,. 45 | .;d0XX0kdlc:;,,,',,,;;:clodkO0KK0Okdl:,'.. 46 | .,coxO0KXXXXXXXKK0OOxdoc:,.. 47 | ... 48 | 49 | You are ban for 50 min !!! -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/favicon.ico -------------------------------------------------------------------------------- /build/images/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/images/logo-192.png -------------------------------------------------------------------------------- /build/images/logo-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/images/logo-310.png -------------------------------------------------------------------------------- /build/images/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/images/logo-512.png -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | Cyber Planning -------------------------------------------------------------------------------- /build/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cyberplanning 8 | 9 | 10 | 139 | 140 | 141 |
142 |
143 |

Cyber Planning

144 |
145 |
146 |
147 | 148 |
149 |
150 |
151 |
152 | 153 | 154 | 155 |
156 |
157 | 158 |
159 |
160 |
161 |
162 | 163 | 173 | 174 | -------------------------------------------------------------------------------- /build/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/logo.gif -------------------------------------------------------------------------------- /build/logo.min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/logo.min.gif -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/logo.png -------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cyber Planning", 3 | "name": "Cyber Planning - What sh*t do I have today?", 4 | "description": "An awesome planning for cyber elite", 5 | "icons": [ 6 | { 7 | "src": "/images/logo-192.png", 8 | "type": "image/png", 9 | "sizes": "192x192" 10 | }, 11 | { 12 | "src": "/images/logo-512.png", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | } 16 | ], 17 | "start_url": "./index.html", 18 | "scope": "/", 19 | "display": "standalone", 20 | "theme_color": "#124368", 21 | "background_color": "#2d3436" 22 | } 23 | -------------------------------------------------------------------------------- /build/precache-manifest.c7e25e80dac3bf390c6499d7c9354ba4.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "ffbeb9f3efe5c0455263ba54ba24bac1", 4 | "url": "/ban" 5 | }, 6 | { 7 | "revision": "4a1e0a4f1d4cd4e478cd60ac7379a2d9", 8 | "url": "/favicon.ico" 9 | }, 10 | { 11 | "revision": "d3453ec30be26a25c42b85c89189475d", 12 | "url": "/images/logo-192.png" 13 | }, 14 | { 15 | "revision": "872165fd9907e3eb4c23fcb70cd33679", 16 | "url": "/images/logo-310.png" 17 | }, 18 | { 19 | "revision": "77299d193444183c7c83c943e8e24925", 20 | "url": "/images/logo-512.png" 21 | }, 22 | { 23 | "revision": "a3e8ed0d7b53a7565999b380f00929b4", 24 | "url": "/index.html" 25 | }, 26 | { 27 | "revision": "00a38aabdddef9cc9ec676261524db1c", 28 | "url": "/login.html" 29 | }, 30 | { 31 | "revision": "001c33c8ebb4d5f363f056882507ed23", 32 | "url": "/logo.gif" 33 | }, 34 | { 35 | "revision": "b754b8e9f39d2771e0d7b9f435074467", 36 | "url": "/logo.html" 37 | }, 38 | { 39 | "revision": "7aa2e885d400768e8687f44e05e393c2", 40 | "url": "/logo.min.gif" 41 | }, 42 | { 43 | "revision": "a33476f7bf7a6c3c704f6eb65ca72eff", 44 | "url": "/logo.min.html" 45 | }, 46 | { 47 | "revision": "d808788972cb43236a851f7c4654021c", 48 | "url": "/logo.min.svg" 49 | }, 50 | { 51 | "revision": "39ca16f95e8c196e936df731cb98f17e", 52 | "url": "/logo.png" 53 | }, 54 | { 55 | "revision": "86b7f0bf73564e82d6c015627381ed7a", 56 | "url": "/logo.svg" 57 | }, 58 | { 59 | "revision": "7a61f54206539d520bb7989a5d9e212b", 60 | "url": "/manifest.json" 61 | }, 62 | { 63 | "revision": "f9aef83d400821e19cfe68392672a7ee", 64 | "url": "/robots.txt" 65 | }, 66 | { 67 | "revision": "66bf26de69d352cf1d35", 68 | "url": "/static/css/main.3953ab90.chunk.css" 69 | }, 70 | { 71 | "revision": "66bf26de69d352cf1d35", 72 | "url": "/static/js/main.66bf26de.chunk.js" 73 | }, 74 | { 75 | "revision": "1b922744246c39b6bc43", 76 | "url": "/static/js/runtime~main.1b922744.js" 77 | }, 78 | { 79 | "revision": "27ac911d0679d9db8050", 80 | "url": "/static/js/vendors~main.27ac911d.chunk.js" 81 | }, 82 | { 83 | "revision": "92e582d852a35a09e98d84b1715d4394", 84 | "url": "/static/media/cybericons.92e582d8.svg" 85 | } 86 | ]); -------------------------------------------------------------------------------- /build/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/build/robots.txt -------------------------------------------------------------------------------- /build/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.c7e25e80dac3bf390c6499d7c9354ba4.js" 18 | ); 19 | 20 | workbox.core.skipWaiting(); 21 | 22 | workbox.core.clientsClaim(); 23 | 24 | /** 25 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 26 | * requests for URLs in the manifest. 27 | * See https://goo.gl/S9QRab 28 | */ 29 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 30 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 31 | -------------------------------------------------------------------------------- /build/static/js/runtime~main.1b922744.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c 2 | 3 | 4 | 5 | Created by FontForge 20190801 at Fri Nov 8 09:39:08 2019 6 | By Hedroed 7 | Copyright (C) 2019 by original authors @ fontello.com 8 | 9 | 10 | 11 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 39 | 43 | 48 | 51 | 54 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /data_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "planning": { 3 | "events": [ 4 | { 5 | "title": "Titre1", 6 | "startDate": "2017-01-02T12:00:00", 7 | "endDate": "2017-01-02T14:00:00", 8 | "groups": [ 9 | "G1", 10 | "G2" 11 | ], 12 | "classrooms": [ 13 | "CR1", 14 | "CR2" 15 | ], 16 | "teachers": [ 17 | "M.Duhdeu", 18 | "Mme.Feef" 19 | ] 20 | }, 21 | { 22 | "title": "Titre2", 23 | "startDate": "2017-01-02T14:00:00", 24 | "endDate": "2017-01-02T16:00:00", 25 | "groups": [ 26 | "G1", 27 | "G2" 28 | ], 29 | "classrooms": [ 30 | "CR1", 31 | "CR2" 32 | ], 33 | "teachers": [ 34 | "M.Duhdeu", 35 | "Mme.Feef" 36 | ] 37 | }, 38 | { 39 | "title": "Titre3", 40 | "startDate": "2017-01-02T16:00:00", 41 | "endDate": "2017-01-02T18:00:00", 42 | "groups": [ 43 | "G1", 44 | "G2" 45 | ], 46 | "classrooms": [ 47 | "CR1", 48 | "CR2" 49 | ], 50 | "teachers": [ 51 | "M.Duhdeu", 52 | "Mme.Feef" 53 | ] 54 | } 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "abadi199/elm-input-extra": "5.2.2", 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.2", 12 | "elm/file": "1.0.5", 13 | "elm/html": "1.0.0", 14 | "elm/http": "1.0.0", 15 | "elm/json": "1.1.3", 16 | "elm/parser": "1.1.0", 17 | "elm/svg": "1.0.1", 18 | "elm/time": "1.0.0", 19 | "justinmimbs/time-extra": "1.1.0", 20 | "rtfeldman/elm-hex": "1.0.0", 21 | "rtfeldman/elm-iso8601-date-strings": "1.1.3", 22 | "truqu/elm-md5": "1.1.0" 23 | }, 24 | "indirect": { 25 | "elm/bytes": "1.0.8", 26 | "elm/regex": "1.0.0", 27 | "elm/url": "1.0.0", 28 | "elm/virtual-dom": "1.0.2", 29 | "elm-community/list-extra": "8.2.2", 30 | "justinmimbs/date": "3.2.0", 31 | "zwilias/elm-utf-tools": "2.0.1" 32 | } 33 | }, 34 | "test-dependencies": { 35 | "direct": {}, 36 | "indirect": {} 37 | } 38 | } -------------------------------------------------------------------------------- /icons/fontello-82f9af8f.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/icons/fontello-82f9af8f.zip -------------------------------------------------------------------------------- /public/ban: -------------------------------------------------------------------------------- 1 | .....'',;;::cccllllllllllllcccc:::;;,,,''...'',,'.. 2 | ..';cldkO00KXNNNNXXXKK000OOkkkkkxxxxxddoooddddddxxxxkkkkOO0XXKx:. 3 | .':ok0KXXXNXK0kxolc:;;,,,,,,,,,,,;;,,,''''''',,''.. .'lOXKd' 4 | .,lx00Oxl:,'............''''''................... ...,;;'. .oKXd. 5 | .ckKKkc'...'',:::;,'.........'',;;::::;,'..........'',;;;,'.. .';;'. 'kNKc. 6 | .:kXXk:. .. .................. .............,:c:'...;:'. .dNNx. 7 | :0NKd, .....''',,,,''.. ',...........',,,'',,::,...,,. .dNNx. 8 | .xXd. .:;'.. ..,' .;,. ...,,'';;'. ... .oNNo 9 | .0K. .;. ;' '; .'...'. .oXX: 10 | .oNO. . ,. . ..',::ccc:;,.. .. lXX: 11 | .dNX: ...... ;. 'cxOKK0OXWWWWWWWNX0kc. :KXd. 12 | .l0N0; ;d0KKKKKXK0ko:... .l0X0xc,...lXWWWWWWWWKO0Kx' ,ONKo. 13 | .lKNKl...'......'. .dXWN0kkk0NWWWWWN0o. :KN0;. .,cokXWWNNNNWNKkxONK: .,:c:. .';;;;:lk0XXx; 14 | :KN0l';ll:'. .,:lodxxkO00KXNWWWX000k. oXNx;:okKX0kdl:::;'',;coxkkd, ...'. ...'''.......',:lxKO:. 15 | oNNk,;c,'',. ...;xNNOc,. ,d0X0xc,. .dOd, ..;dOKXK00000Ox:. ..''dKO, 16 | 'KW0,:,.,:..,oxkkkdl;'. 'KK' .. .dXX0o:'....,:oOXNN0d;.'. ..,lOKd. .. ;KXl. 17 | ;XNd,; ;. l00kxoooxKXKx:..ld: ;KK' .:dkO000000Okxl;. c0; :KK; . ;XXc 18 | 'XXdc. :. .. '' 'kNNNKKKk, .,dKNO. .... .'c0NO' :X0. ,. xN0. 19 | .kNOc' ,. .00. ..''... .l0X0d;. 'dOkxo;... .;okKXK0KNXx;. .0X: ,. lNX' 20 | ,KKdl .c, .dNK, .;xXWKc. .;:coOXO,,'....... .,lx0XXOo;...oNWNXKk:.'KX; ' dNX. 21 | :XXkc'.... .dNWXl .';l0NXNKl. ,lxkkkxo' .cK0. ..;lx0XNX0xc. ,0Nx'.','.kXo ., ,KNx. 22 | cXXd,,;:, .oXWNNKo' .'.. .'.'dKk; .cooollox;.xXXl ..,cdOKXXX00NXc. 'oKWK' ;k: .l. ,0Nk. 23 | cXNx. . ,KWX0NNNXOl'. .o0Ooldk; .:c;.':lxOKKK0xo:,.. ;XX: .,lOXWWXd. . .':,.lKXd. 24 | lXNo cXWWWXooNWNXKko;'.. .lk0x; ...,:ldk0KXNNOo:,.. ,OWNOxO0KXXNWNO, ....'l0Xk, 25 | .dNK. oNWWNo.cXK;;oOXNNXK0kxdolllllooooddxk00KKKK0kdoc:c0No .'ckXWWWNXkc,;kNKl. .,kXXk, 26 | 'KXc .dNWWX;.xNk. .kNO::lodxkOXWN0OkxdlcxNKl,.. oN0'..,:ox0XNWWNNWXo. ,ONO' .o0Xk; 27 | .ONo oNWWN0xXWK, .oNKc .ONx. ;X0. .:XNKKNNWWWWNKkl;kNk. .cKXo. .ON0; 28 | .xNd cNWWWWWWWWKOkKNXxl:,'...;0Xo'.....'lXK;...',:lxk0KNWWWWNNKOd:.. lXKclON0: .xNk. 29 | .dXd ;XWWWWWWWWWWWWWWWWWWNNNNNWWNNNNNNNNNWWNNNNNNWWWWWNXKNNk;.. .dNWWXd. cXO. 30 | .xXo .ONWNWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWNNK0ko:'..OXo 'l0NXx, :KK, 31 | .OXc :XNk0NWXKNWWWWWWWWWWWWWWWWWWWWWNNNX00NNx:'.. lXKc. 'lONN0l. .oXK: 32 | .KX; .dNKoON0;lXNkcld0NXo::cd0NNO:;,,'.. .0Xc lXXo..'l0NNKd,. .c0Nk, 33 | :XK. .xNX0NKc.cXXl ;KXl .dN0. .0No .xNXOKNXOo,. .l0Xk;. 34 | .dXk. .lKWN0d::OWK; lXXc .OX: .ONx. . .,cdk0XNXOd;. .'''....;c:'..;xKXx, 35 | .0No .:dOKNNNWNKOxkXWXo:,,;ONk;,,,,,;c0NXOxxkO0XXNXKOdc,. ..;::,...;lol;..:xKXOl. 36 | ,XX: ..';cldxkOO0KKKXXXXXXXXXXKKKKK00Okxdol:;'.. .';::,..':llc,..'lkKXkc. 37 | :NX' . '' .................. .,;:;,',;ccc;'..'lkKX0d;. 38 | lNK. .; ,lc,. ................ ..,,;;;;;;:::,....,lkKX0d:. 39 | .oN0. .'. .;ccc;,'.... ....'',;;;;;;;;;;'.. .;oOXX0d:. 40 | .dN0. .;;,.. .... ..''''''''.... .:dOKKko;. 41 | lNK' ..,;::;;,'......................... .;d0X0kc'. 42 | .xXO' .;oOK0x:. 43 | .cKKo. .,:oxkkkxk0K0xc'. 44 | .oKKkc,. .';cok0XNNNX0Oxoc,. 45 | .;d0XX0kdlc:;,,,',,,;;:clodkO0KK0Okdl:,'.. 46 | .,coxO0KXXXXXXXKK0OOxdoc:,.. 47 | ... 48 | 49 | You are ban for 50 min !!! -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/images/logo-192.png -------------------------------------------------------------------------------- /public/images/logo-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/images/logo-310.png -------------------------------------------------------------------------------- /public/images/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/images/logo-512.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Cyber Planning 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cyberplanning 8 | 9 | 10 | 139 | 140 | 141 |
142 |
143 |

Cyber Planning

144 |
145 | 161 |
162 | 163 | 173 | 174 | -------------------------------------------------------------------------------- /public/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/logo.gif -------------------------------------------------------------------------------- /public/logo.min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/logo.min.gif -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cyber Planning", 3 | "name": "Cyber Planning - What sh*t do I have today?", 4 | "description": "An awesome planning for cyber elite", 5 | "icons": [ 6 | { 7 | "src": "/images/logo-192.png", 8 | "type": "image/png", 9 | "sizes": "192x192" 10 | }, 11 | { 12 | "src": "/images/logo-512.png", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | } 16 | ], 17 | "start_url": "./index.html", 18 | "scope": "/", 19 | "display": "standalone", 20 | "theme_color": "#124368", 21 | "background_color": "#2d3436" 22 | } 23 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/public/robots.txt -------------------------------------------------------------------------------- /src/Calendar/Calendar.elm: -------------------------------------------------------------------------------- 1 | module Calendar.Calendar exposing (State, init, page, update, view) 2 | 3 | import Calendar.Day as Day 4 | import Calendar.Event exposing (Event) 5 | import Calendar.JourFerie exposing (getAllJourFerie) 6 | import Calendar.Msg exposing (InternalState, Msg(..), TimeSpan(..)) 7 | import Calendar.Week as Week 8 | import Html exposing (Html, div) 9 | import Html.Attributes exposing (class) 10 | import MyTime 11 | import Time exposing (Posix) 12 | import Time.Extra as TimeExtra 13 | 14 | 15 | type alias State = 16 | InternalState 17 | 18 | 19 | init : TimeSpan -> Posix -> State 20 | init timeSpan viewing = 21 | { timeSpan = timeSpan 22 | , viewing = viewing 23 | , hover = Nothing 24 | , position = Nothing 25 | , selected = Nothing 26 | , joursFeries = getAllJourFerie (MyTime.toYear viewing) 27 | } 28 | 29 | 30 | update : Msg -> State -> State 31 | update msg state = 32 | -- case Debug.log "msg" msg of 33 | case msg of 34 | PageBack -> 35 | page -1 state 36 | 37 | PageForward -> 38 | page 1 state 39 | 40 | WeekBack -> 41 | page -7 state 42 | 43 | WeekForward -> 44 | page 7 state 45 | 46 | ChangeTimeSpan timeSpan -> 47 | { state | timeSpan = timeSpan } 48 | 49 | ChangeViewing viewing -> 50 | { state | viewing = viewing } 51 | 52 | EventClick eventId pos -> 53 | { state | selected = Just eventId, position = Just pos } 54 | 55 | EventMouseEnter eventId pos -> 56 | { state | hover = Just eventId, position = Just pos } 57 | 58 | EventMouseLeave _ -> 59 | { state | hover = Nothing, selected = Nothing } 60 | 61 | 62 | page : Int -> State -> State 63 | page step state = 64 | let 65 | { timeSpan, viewing } = 66 | state 67 | 68 | interval = 69 | case timeSpan of 70 | Week -> 71 | TimeExtra.Week 72 | 73 | AllWeek -> 74 | TimeExtra.Week 75 | 76 | Day -> 77 | TimeExtra.Day 78 | in 79 | { state | viewing = MyTime.add interval step viewing, hover = Nothing, selected = Nothing } 80 | 81 | 82 | view : List Event -> Int -> State -> Html Msg 83 | view events columns state = 84 | let 85 | calendarView = 86 | case state.timeSpan of 87 | Week -> 88 | Week.view state columns events 89 | 90 | AllWeek -> 91 | Week.viewAll state columns events 92 | 93 | Day -> 94 | Day.view state columns events 95 | in 96 | div 97 | [ class "calendar--calendar" ] 98 | [ calendarView ] 99 | -------------------------------------------------------------------------------- /src/Calendar/Day.elm: -------------------------------------------------------------------------------- 1 | module Calendar.Day exposing (view, viewAllDayCell, viewDate, viewDayEvent, viewDayEvents, viewDayHeader, viewDaySlot, viewDaySlotGroup, viewHourSlot, viewTimeGutter, viewTimeGutterHeader, viewTimeSlot, viewTimeSlotGroup) 2 | 3 | import Calendar.Event exposing (Event, eventSegment, rangeDescription) 4 | import Calendar.Helpers as Helpers 5 | import Calendar.JourFerie exposing (jourFerie) 6 | import Calendar.Msg exposing (InternalState, Msg(..)) 7 | import Html exposing (Html, div, button, text, span) 8 | import Html.Attributes exposing (class) 9 | import Html.Events exposing (onClick) 10 | import MyTime 11 | import Time exposing (Posix) 12 | import Time.Extra as TimeExtra 13 | 14 | 15 | view : InternalState -> Int -> List Event -> Html Msg 16 | view state columns events = 17 | div [ class "calendar--day" ] 18 | [ div [ class "calendar--day-content" ] 19 | [ viewTimeGutter state.viewing 20 | , div [ class "calendar--day" ] 21 | [ viewDayHeader state.viewing 22 | , viewDaySlot state columns events 23 | ] 24 | ] 25 | ] 26 | 27 | 28 | viewDate : Posix -> Html Msg 29 | viewDate day = 30 | div [ class "calendar--date-header" ] 31 | [ button [ class "calendar--navigations-week", onClick WeekBack ] [ text "<<" ] 32 | , div [ class "calendar--date-header-content" ] 33 | [ button [ class "calendar--navigations-day", onClick PageBack ] [ text "<" ] 34 | , span [ class "calendar--date" ] [ text <| Helpers.dateString day ] 35 | , button [ class "calendar--navigations-day", onClick PageForward ] [ text ">" ] 36 | ] 37 | , button [ class "calendar--navigations-week", onClick WeekForward ] [ text ">>" ] 38 | ] 39 | 40 | 41 | viewDayHeader : Posix -> Html Msg 42 | viewDayHeader day = 43 | div [ class "calendar--day-header" ] 44 | [ viewDate day 45 | ] 46 | 47 | 48 | viewTimeGutter : Posix -> Html Msg 49 | viewTimeGutter viewing = 50 | Helpers.hours 51 | |> List.indexedMap (viewTimeSlotGroup viewing) 52 | |> (::) (viewTimeGutterHeader viewing) 53 | |> div [ class "calendar--time-gutter" ] 54 | 55 | 56 | viewTimeGutterHeader : Posix -> Html Msg 57 | viewTimeGutterHeader viewing = 58 | let 59 | date = 60 | viewing 61 | |> MyTime.ceiling TimeExtra.Sunday 62 | 63 | weekNum = 64 | MyTime.diff TimeExtra.Week 65 | (MyTime.floor TimeExtra.Year date) 66 | date 67 | |> (+) 1 68 | |> String.fromInt 69 | in 70 | div [ class "calendar--date-header", class "calendar--date-header-weeknum" ] 71 | [ span [ class "calendar--date" ] [ text weekNum ] 72 | ] 73 | 74 | 75 | viewTimeGutterZone : Posix -> Html Msg 76 | viewTimeGutterZone viewing = 77 | let 78 | offset = 79 | viewing 80 | |> MyTime.toOffset 81 | 82 | zone = 83 | offset 84 | / 60 85 | |> floor 86 | |> String.fromInt 87 | |> (++) "GMT+" 88 | in 89 | div [ class "calendar--date-header-zone" ] 90 | [ span [] [ text zone ] 91 | ] 92 | 93 | 94 | viewTimeSlotGroup : Posix -> Int -> String -> Html Msg 95 | viewTimeSlotGroup viewing idx date = 96 | div [ class "calendar--time-slot-group" ] 97 | [ if idx == 0 then 98 | viewTimeGutterZone viewing 99 | 100 | else 101 | text "" 102 | , viewHourSlot date 103 | , div [ class "calendar--time-slot" ] [] 104 | ] 105 | 106 | 107 | viewHourSlot : String -> Html Msg 108 | viewHourSlot date = 109 | div [ class "calendar--hour-slot" ] 110 | [ span [ class "calendar--time-slot-text" ] [ text date ] ] 111 | 112 | 113 | viewDaySlot : InternalState -> Int -> List Event -> Html Msg 114 | viewDaySlot state columns events = 115 | Helpers.hours 116 | |> List.map viewDaySlotGroup 117 | |> (\b a -> a ++ b) (viewDayEvents state columns events state.viewing) 118 | |> div [ class "calendar--day-slot" ] 119 | 120 | 121 | viewDaySlotGroup : String -> Html Msg 122 | viewDaySlotGroup date = 123 | div [ class "calendar--time-slot-group" ] 124 | [ viewTimeSlot date 125 | , viewTimeSlot date 126 | ] 127 | 128 | 129 | viewTimeSlot : String -> Html Msg 130 | viewTimeSlot _ = 131 | div 132 | [ class "calendar--time-slot" ] 133 | [] 134 | 135 | 136 | viewDayEvents : InternalState -> Int -> List Event -> Posix -> List (Html Msg) 137 | viewDayEvents state columns events day = 138 | let 139 | extra = 140 | case jourFerie state.joursFeries day of 141 | Just name -> 142 | text name 143 | |> List.singleton 144 | |> div [ class "calendar--jour-ferie" ] 145 | |> List.singleton 146 | 147 | Nothing -> 148 | [] 149 | 150 | eventsHtml = 151 | List.filterMap (viewDayEvent columns day) events 152 | in 153 | extra ++ eventsHtml 154 | 155 | 156 | viewDayEvent : Int -> Posix -> Event -> Maybe (Html Msg) 157 | viewDayEvent columns day event = 158 | if rangeDescription event.startTime event.endTime TimeExtra.Day day then 159 | Just <| eventSegment columns event 160 | 161 | else 162 | Nothing 163 | 164 | 165 | viewAllDayCell : List Posix -> Html Msg 166 | viewAllDayCell days = 167 | let 168 | viewAllDayText = 169 | div [ class "calendar--all-day-text" ] [ text "All day" ] 170 | 171 | viewAllDay _ = 172 | div [ class "calendar--all-day" ] 173 | [] 174 | in 175 | div [ class "calendar--all-day-cell" ] 176 | (viewAllDayText :: List.map viewAllDay days) 177 | -------------------------------------------------------------------------------- /src/Calendar/Event.elm: -------------------------------------------------------------------------------- 1 | module Calendar.Event exposing (Event, PositionMode(..), Style, cellWidth, eventSegment, eventStyling, offsetLength, offsetPercentage, percentDay, rangeDescription, rowSegment, styleDayEvent, styleRowSegment) 2 | 3 | -- import String.Extra 4 | 5 | import Calendar.Msg exposing (Msg(..), TimeSpan(..), onClick, onMouseEnter) 6 | import Html exposing (Html, div, text) 7 | import Html.Attributes exposing (attribute, class, classList, style) 8 | import Html.Events exposing (onMouseLeave) 9 | import MyTime 10 | import String 11 | import Time exposing (Posix, Weekday(..)) 12 | import Time.Extra as TimeExtra 13 | 14 | 15 | type alias Style = 16 | { eventColor : String 17 | , textColor : String 18 | } 19 | 20 | 21 | type alias Event = 22 | { toId : String 23 | , title : String 24 | , startTime : Posix 25 | , endTime : Posix 26 | , description : List String 27 | , source : String 28 | , style : Style 29 | , position : PositionMode 30 | } 31 | 32 | 33 | type PositionMode 34 | = All 35 | | Column Int Int 36 | 37 | 38 | rangeDescription : Posix -> Posix -> TimeExtra.Interval -> Posix -> Bool 39 | rangeDescription start end interval date = 40 | let 41 | -- Fix : floor and ceiling return same Time if it is midnight 42 | day = 43 | MyTime.add TimeExtra.Millisecond 1 date 44 | 45 | begInterval = 46 | MyTime.floor interval day 47 | 48 | endInterval = 49 | MyTime.ceiling interval day 50 | 51 | startsThisInterval = 52 | isBetween begInterval endInterval start 53 | 54 | endsThisInterval = 55 | isBetween begInterval endInterval end 56 | in 57 | startsThisInterval && endsThisInterval 58 | 59 | 60 | eventStyling : 61 | Int 62 | -> Event 63 | -> List ( String, Bool ) 64 | -> List (Html.Attribute msg) 65 | eventStyling columns event customClasses = 66 | let 67 | eventStart = 68 | event.startTime 69 | 70 | eventEnd = 71 | event.endTime 72 | 73 | colorBg = 74 | event.style.eventColor 75 | 76 | colorFg = 77 | event.style.textColor 78 | 79 | eventTitle = 80 | escapeTitle event.title 81 | 82 | classes = 83 | "calendar--event calendar--event-starts-and-ends" 84 | 85 | extraStyle = 86 | if String.isEmpty event.source then 87 | [] 88 | 89 | else 90 | [ style "border-color" colorFg ] 91 | 92 | styles = 93 | styleDayEvent columns eventStart eventEnd event.position 94 | ++ styleColorDayEvent eventTitle colorFg colorBg 95 | ++ extraStyle 96 | in 97 | classList (( classes, True ) :: customClasses) :: styles 98 | 99 | 100 | fractionalDay : Posix -> Float 101 | fractionalDay time = 102 | let 103 | hours = 104 | MyTime.toHour time 105 | 106 | minutes = 107 | MyTime.toMinute time 108 | 109 | seconds = 110 | MyTime.toSecond time 111 | in 112 | toFloat ((hours * 3600) + (minutes * 60) + seconds) / (24 * 3600) 113 | 114 | 115 | percentDay : Posix -> Float -> Float -> Float 116 | percentDay date min max = 117 | (fractionalDay date - min) / (max - min) 118 | 119 | 120 | styleDayEvent : Int -> Posix -> Posix -> PositionMode -> List (Html.Attribute msg) 121 | styleDayEvent columns start end position = 122 | let 123 | ( left, width ) = 124 | case position of 125 | All -> 126 | ( "0", "96%" ) 127 | 128 | Column idx size -> 129 | let 130 | fractionUnit = 131 | 96 / toFloat columns 132 | 133 | fractionSize = 134 | fractionUnit * toFloat size 135 | 136 | l = 137 | idx 138 | |> toFloat 139 | |> (*) fractionUnit 140 | |> String.fromFloat 141 | |> (\x -> x ++ "%") 142 | 143 | w = 144 | fractionSize 145 | |> String.fromFloat 146 | |> (\x -> x ++ "%") 147 | in 148 | ( l, w ) 149 | 150 | startPercent = 151 | 100 * percentDay start (7 / 24) (21 / 24) 152 | 153 | endPercent = 154 | 100 * percentDay end (7 / 24) (21 / 24) 155 | 156 | height = 157 | (String.fromFloat <| endPercent - startPercent) ++ "%" 158 | 159 | startPercentage = 160 | String.fromFloat startPercent ++ "%" 161 | in 162 | [ style "top" startPercentage 163 | , style "height" height 164 | , style "left" left 165 | , style "margin" "0 6px" 166 | , style "width" width 167 | , style "position" "absolute" 168 | ] 169 | 170 | 171 | styleColorDayEvent : String -> String -> String -> List (Html.Attribute msg) 172 | styleColorDayEvent title fg bg = 173 | [ style "background-color" bg 174 | , style "color" fg 175 | , attribute "data-title" title 176 | , attribute "data-color" fg 177 | ] 178 | 179 | 180 | eventSegment : Int -> Event -> Html Msg 181 | eventSegment columns event = 182 | let 183 | eventId = 184 | event.toId 185 | 186 | classes = 187 | [ ( "calendar--event-content", True ) 188 | ] 189 | 190 | title = 191 | [ text event.title ] 192 | 193 | childs = 194 | List.map viewSub event.description 195 | in 196 | div [] 197 | [ div 198 | ([ onMouseEnter <| EventMouseEnter eventId 199 | , onMouseLeave <| EventMouseLeave eventId 200 | , onClick <| EventClick eventId 201 | ] 202 | ++ eventStyling columns event classes 203 | ) 204 | (div [ class "calendar--event-title" ] title :: childs) 205 | ] 206 | 207 | 208 | viewSub : String -> Html Msg 209 | viewSub val = 210 | div [ class "calendar--event-sub" ] [ text val ] 211 | 212 | 213 | cellWidth : Float 214 | cellWidth = 215 | 100.0 / 7 216 | 217 | 218 | offsetLength : Posix -> Float 219 | offsetLength date = 220 | MyTime.toWeekday date 221 | |> MyTime.weekdayToNumber 222 | |> modBy 7 223 | |> toFloat 224 | |> (*) cellWidth 225 | 226 | 227 | offsetPercentage : Posix -> String 228 | offsetPercentage date = 229 | (offsetLength date 230 | |> String.fromFloat 231 | ) 232 | ++ "%" 233 | 234 | 235 | styleRowSegment : String -> List (Html.Attribute msg) 236 | styleRowSegment widthPercentage = 237 | [ style "flex-basis" widthPercentage 238 | , style "max-width" widthPercentage 239 | ] 240 | 241 | 242 | rowSegment : String -> List (Html Msg) -> Html Msg 243 | rowSegment widthPercentage children = 244 | div (styleRowSegment widthPercentage) children 245 | 246 | 247 | isBetween : Posix -> Posix -> Posix -> Bool 248 | isBetween start end current = 249 | let 250 | startInt = 251 | Time.posixToMillis start 252 | 253 | endInt = 254 | Time.posixToMillis end 255 | 256 | currentInt = 257 | Time.posixToMillis current 258 | in 259 | startInt <= currentInt && endInt >= currentInt 260 | 261 | 262 | escapeTitle : String -> String 263 | escapeTitle = 264 | always "" 265 | -------------------------------------------------------------------------------- /src/Calendar/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Calendar.Helpers exposing (colorToHex, computeColor, dateString, dayRangeOfAllWeek, dayRangeOfWeek, hours, noBright) 2 | 3 | import Hex 4 | import MD5 5 | import MyTime 6 | import Time exposing (Posix, Weekday(..)) 7 | import Time.Extra as TimeExtra 8 | import Vendor.Color exposing (Color) 9 | 10 | 11 | dateString : Posix -> String 12 | dateString date = 13 | let 14 | weekday = 15 | MyTime.toWeekday date 16 | 17 | weekname = 18 | case weekday of 19 | Mon -> 20 | "Lundi" 21 | 22 | Tue -> 23 | "Mardi" 24 | 25 | Wed -> 26 | "Mercredi" 27 | 28 | Thu -> 29 | "Jeudi" 30 | 31 | Fri -> 32 | "Vendredi" 33 | 34 | Sat -> 35 | "Samedi" 36 | 37 | Sun -> 38 | "Dimanche" 39 | 40 | day = 41 | MyTime.toDay date 42 | |> String.fromInt 43 | in 44 | weekname ++ " " ++ day 45 | 46 | 47 | hours : List String 48 | hours = 49 | [ "7:00" 50 | , "8:00" 51 | , "9:00" 52 | , "10:00" 53 | , "11:00" 54 | , "12:00" 55 | , "13:00" 56 | , "14:00" 57 | , "15:00" 58 | , "16:00" 59 | , "17:00" 60 | , "18:00" 61 | , "19:00" 62 | , "20:00" 63 | ] 64 | 65 | 66 | dayRangeOfWeek : Posix -> List Posix 67 | dayRangeOfWeek date = 68 | let 69 | weekDate = 70 | date 71 | -- move to middle week because week-end are not showned 72 | |> MyTime.add TimeExtra.Day 2 73 | |> MyTime.floor TimeExtra.Monday 74 | in 75 | MyTime.range TimeExtra.Day 76 | 1 77 | weekDate 78 | (MyTime.ceiling TimeExtra.Saturday weekDate) 79 | 80 | 81 | dayRangeOfAllWeek : Posix -> List Posix 82 | dayRangeOfAllWeek date = 83 | let 84 | weekDate = 85 | date 86 | -- Fix : range return no value if it is Monday midnight 87 | |> MyTime.floor TimeExtra.Day 88 | |> MyTime.add TimeExtra.Millisecond 1 89 | in 90 | MyTime.range TimeExtra.Day 91 | 1 92 | (MyTime.floor TimeExtra.Monday weekDate) 93 | (MyTime.ceiling TimeExtra.Monday weekDate) 94 | 95 | 96 | computeColor : String -> String 97 | computeColor text = 98 | let 99 | hex = 100 | String.dropRight 1 text 101 | |> String.toUpper 102 | |> MD5.hex 103 | |> String.right 6 104 | 105 | red = 106 | String.slice 0 2 hex 107 | |> Hex.fromString 108 | |> Result.withDefault 0 109 | 110 | green = 111 | String.slice 2 4 hex 112 | |> Hex.fromString 113 | |> Result.withDefault 0 114 | 115 | blue = 116 | String.slice 4 6 hex 117 | |> Hex.fromString 118 | |> Result.withDefault 0 119 | in 120 | Vendor.Color.rgb red green blue 121 | |> noBright 122 | |> colorToHex 123 | 124 | 125 | colorToHex : Color -> String 126 | colorToHex color = 127 | let 128 | rgb = 129 | Vendor.Color.toRgb color 130 | 131 | toHex = 132 | Hex.toString >> String.padLeft 2 '0' 133 | in 134 | "#" ++ toHex rgb.red ++ toHex rgb.green ++ toHex rgb.blue 135 | 136 | 137 | noBright : Color -> Color 138 | noBright color = 139 | let 140 | hsl = 141 | Vendor.Color.toHsl color 142 | 143 | -- hsl.lightness * 0.7 144 | newLightness = 145 | if hsl.lightness > 0.4 then 146 | 0.4 147 | 148 | else 149 | hsl.lightness 150 | 151 | in 152 | Vendor.Color.hsl hsl.hue hsl.saturation newLightness 153 | -------------------------------------------------------------------------------- /src/Calendar/JourFerie.elm: -------------------------------------------------------------------------------- 1 | module Calendar.JourFerie exposing (getAllJourFerie, jourFerie, jourFerieName) 2 | 3 | import Dict 4 | import MyTime 5 | import Time exposing (Month(..), Posix) 6 | import Time.Extra as TimeExtra exposing (Parts) 7 | 8 | 9 | jourFerie : Dict.Dict String Posix -> Posix -> Maybe String 10 | jourFerie joursFeries day = 11 | let 12 | dd = 13 | MyTime.floor TimeExtra.Day day 14 | in 15 | Dict.filter (\_ v -> v == dd) joursFeries 16 | |> Dict.keys 17 | |> List.head 18 | 19 | 20 | jourFerieName : Dict.Dict String Posix -> String -> Maybe Posix 21 | jourFerieName joursFeries name = 22 | Dict.get name joursFeries 23 | 24 | 25 | getAllJourFerie : Int -> Dict.Dict String Posix 26 | getAllJourFerie year = 27 | let 28 | -- Paques 29 | n = 30 | modBy 19 year 31 | 32 | c = 33 | year // 100 34 | 35 | u = 36 | modBy 100 year 37 | 38 | s = 39 | c // 4 40 | 41 | t = 42 | modBy 4 c 43 | 44 | p = 45 | (c + 8) // 25 46 | 47 | q = 48 | (c - p + 1) // 3 49 | 50 | e = 51 | modBy 30 (19 * n + c - s - q + 15) 52 | 53 | b = 54 | u // 4 55 | 56 | d = 57 | modBy 4 u 58 | 59 | l = 60 | modBy 7 (2 * t + 2 * b - e - d + 32) 61 | 62 | h = 63 | (n + 11 * e + 22 * l) // 451 64 | 65 | n0 = 66 | e + l - 7 * h + 114 67 | 68 | m = 69 | n0 // 31 70 | 71 | j = 72 | modBy 31 n0 73 | 74 | paques = 75 | MyTime.partsToPosix (Parts year (numberToMonth m) (j + 1) 0 0 0 0) 76 | 77 | lundiPaques = 78 | MyTime.add TimeExtra.Day 1 paques 79 | 80 | ascension = 81 | MyTime.add TimeExtra.Day 39 paques 82 | 83 | pentecote = 84 | MyTime.add TimeExtra.Day 50 paques 85 | 86 | jourDeLAn = 87 | MyTime.partsToPosix (Parts year Jan 1 0 0 0 0) 88 | 89 | feteDuTravail = 90 | MyTime.partsToPosix (Parts year May 1 0 0 0 0) 91 | 92 | victoireAllies = 93 | MyTime.partsToPosix (Parts year May 8 0 0 0 0) 94 | 95 | feteNationale = 96 | MyTime.partsToPosix (Parts year Jul 14 0 0 0 0) 97 | 98 | assomption = 99 | MyTime.partsToPosix (Parts year Aug 15 0 0 0 0) 100 | 101 | toussaint = 102 | MyTime.partsToPosix (Parts year Nov 1 0 0 0 0) 103 | 104 | armistice = 105 | MyTime.partsToPosix (Parts year Nov 11 0 0 0 0) 106 | 107 | noel = 108 | MyTime.partsToPosix (Parts year Dec 25 0 0 0 0) 109 | in 110 | [ ( "Pâques", paques ) 111 | , ( "Lundi de Pâques", lundiPaques ) 112 | , ( "Ascension", ascension ) 113 | , ( "Pentecôte", pentecote ) 114 | , ( "Jour de l'an", jourDeLAn ) 115 | , ( "Fête du Travail", feteDuTravail ) 116 | , ( "Victoire des allies", victoireAllies ) 117 | , ( "Fête Nationale", feteNationale ) 118 | , ( "Assomption", assomption ) 119 | , ( "La Toussaint", toussaint ) 120 | , ( "Armistice", armistice ) 121 | , ( "Noël", noel ) 122 | ] 123 | |> Dict.fromList 124 | 125 | 126 | numberToMonth : Int -> Month 127 | numberToMonth number = 128 | case max 1 number of 129 | 1 -> 130 | Jan 131 | 132 | 2 -> 133 | Feb 134 | 135 | 3 -> 136 | Mar 137 | 138 | 4 -> 139 | Apr 140 | 141 | 5 -> 142 | May 143 | 144 | 6 -> 145 | Jun 146 | 147 | 7 -> 148 | Jul 149 | 150 | 8 -> 151 | Aug 152 | 153 | 9 -> 154 | Sep 155 | 156 | 10 -> 157 | Oct 158 | 159 | 11 -> 160 | Nov 161 | 162 | _ -> 163 | Dec 164 | -------------------------------------------------------------------------------- /src/Calendar/Msg.elm: -------------------------------------------------------------------------------- 1 | module Calendar.Msg exposing (InternalState, Msg(..), Position, TimeSpan(..), onClick, onMouseEnter, position) 2 | 3 | import Dict exposing (Dict) 4 | import Html exposing (Attribute) 5 | import Html.Events exposing (on) 6 | import Json.Decode as Json 7 | import Time exposing (Posix) 8 | 9 | 10 | type TimeSpan 11 | = Week 12 | | AllWeek 13 | | Day 14 | 15 | 16 | type Msg 17 | = PageBack 18 | | PageForward 19 | | WeekForward 20 | | WeekBack 21 | | ChangeTimeSpan TimeSpan 22 | | ChangeViewing Posix 23 | | EventClick String Position 24 | | EventMouseEnter String Position 25 | | EventMouseLeave String 26 | 27 | 28 | type alias InternalState = 29 | { timeSpan : TimeSpan 30 | , viewing : Posix 31 | , hover : Maybe String 32 | , position : Maybe Position 33 | , selected : Maybe String 34 | , joursFeries : Dict String Posix 35 | } 36 | 37 | 38 | type alias Position = 39 | { x : Int 40 | , y : Int 41 | } 42 | 43 | 44 | {-| The decoder used to extract a `Position` from a JavaScript mouse event. 45 | -} 46 | position : Json.Decoder Position 47 | position = 48 | Json.map2 Position 49 | (Json.field "pageX" Json.int) 50 | (Json.field "pageY" Json.int) 51 | 52 | 53 | onMouseEnter : (Position -> msg) -> Attribute msg 54 | onMouseEnter msg = 55 | on "mouseenter" (Json.map msg position) 56 | 57 | 58 | onClick : (Position -> msg) -> Attribute msg 59 | onClick msg = 60 | on "click" (Json.map msg position) 61 | -------------------------------------------------------------------------------- /src/Calendar/Week.elm: -------------------------------------------------------------------------------- 1 | module Calendar.Week exposing (view, viewAll, viewWeekContent, viewWeekDay) 2 | 3 | import Calendar.Day exposing (viewDayEvents, viewDaySlotGroup, viewTimeGutter) 4 | import Calendar.Event exposing (Event) 5 | import Calendar.Helpers as Helpers 6 | import Calendar.Msg exposing (InternalState, Msg(..)) 7 | import Html exposing (Html, div, span, text) 8 | import Html.Attributes exposing (class) 9 | import Time exposing (Posix) 10 | 11 | 12 | view : InternalState -> Int -> List Event -> Html Msg 13 | view state columns events = 14 | Helpers.dayRangeOfWeek state.viewing 15 | |> viewDays state columns events 16 | 17 | 18 | viewAll : InternalState -> Int -> List Event -> Html Msg 19 | viewAll state columns events = 20 | Helpers.dayRangeOfAllWeek state.viewing 21 | |> viewDays state columns events 22 | 23 | 24 | viewDays : InternalState -> Int -> List Event -> List Posix -> Html Msg 25 | viewDays state columns events weekRange = 26 | div [ class "calendar--week" ] 27 | [ viewWeekContent state columns events weekRange 28 | ] 29 | 30 | 31 | viewWeekContent : 32 | InternalState 33 | -> Int 34 | -> List Event 35 | -> List Posix 36 | -> Html Msg 37 | viewWeekContent state columns events days = 38 | let 39 | timeGutter = 40 | viewTimeGutter state.viewing 41 | 42 | weekDays = 43 | List.map (viewWeekDay state columns events) days 44 | in 45 | div [ class "calendar--week-content" ] 46 | (timeGutter :: weekDays) 47 | 48 | 49 | viewWeekDay : InternalState -> Int -> List Event -> Posix -> Html Msg 50 | viewWeekDay state columns events day = 51 | let 52 | viewDaySlots = 53 | Helpers.hours 54 | |> List.map viewDaySlotGroup 55 | 56 | dayEvents = 57 | viewDayEvents state columns events day 58 | in 59 | div [ class "calendar--dates" ] 60 | [ div [ class "calendar--date-header" ] 61 | [ span [ class "calendar--date" ] [ text <| Helpers.dateString day ] ] 62 | , div [ class "calendar--day-slot" ] 63 | (viewDaySlots ++ dayEvents) 64 | ] 65 | 66 | 67 | 68 | -- viewWeekHeader : List Posix -> Html Msg 69 | -- viewWeekHeader days = 70 | -- div [ class "calendar--week-header" ] 71 | -- [ viewDates days ] 72 | -- viewDates : List Posix -> Html Msg 73 | -- viewDates days = 74 | -- div [ class "calendar--dates-header" ] 75 | -- [ viewTimeGutterHeader 76 | -- , div [ class "calendar--dates" ] <| List.map viewDate days 77 | -- ] 78 | -- viewDate : Posix -> Html Msg 79 | -- viewDate day = 80 | -- div [ class "calendar--date-header" ] 81 | -- [ span [ class "calendar--date" ] [ text <| Helpers.dateString day ] ] 82 | -------------------------------------------------------------------------------- /src/Config.elm: -------------------------------------------------------------------------------- 1 | module Config exposing (allGroups, apiUrl, firstGroup, minWeekWidth, enableEasterEgg, enablePersonnelCal) 2 | 3 | import Cyberplanning.Types exposing (Collection(..), Group) 4 | 5 | 6 | enableEasterEgg : Bool 7 | enableEasterEgg = 8 | False 9 | 10 | 11 | enablePersonnelCal : Bool 12 | enablePersonnelCal = 13 | False 14 | 15 | 16 | apiUrl : String 17 | apiUrl = 18 | -- "http://localhost:3001/graphql/" 19 | "https://cyberplanning.fr/graphql/" 20 | 21 | 22 | allGroups : List Group 23 | allGroups = 24 | [ { name = "Cyber1 TD1 TP1", slug = "111", collection = Cyber, id = 0 } 25 | , { name = "Cyber1 TD1 TP2", slug = "112", collection = Cyber, id = 1 } 26 | , { name = "Cyber1 TD2 TP3", slug = "121", collection = Cyber, id = 2 } 27 | , { name = "Cyber1 TD2 TP4", slug = "122", collection = Cyber, id = 3 } 28 | , { name = "Cyber1 TD3 TP5", slug = "131", collection = Cyber, id = 18 } 29 | , { name = "Cyber1 TD3 TP6", slug = "132", collection = Cyber, id = 19 } 30 | , { name = "Cyber2 TD1 TP1", slug = "211", collection = Cyber, id = 4 } 31 | , { name = "Cyber2 TD1 TP2", slug = "212", collection = Cyber, id = 5 } 32 | , { name = "Cyber2 TD2 TP3", slug = "221", collection = Cyber, id = 6 } 33 | , { name = "Cyber2 TD2 TP4", slug = "222", collection = Cyber, id = 7 } 34 | , { name = "Cyber3 TD1 TP1", slug = "311", collection = Cyber, id = 8 } 35 | , { name = "Cyber3 TD1 TP2", slug = "312", collection = Cyber, id = 9 } 36 | , { name = "Cyber3 TD2 TP3", slug = "321", collection = Cyber, id = 10 } 37 | , { name = "Cyber3 TD2 TP4", slug = "322", collection = Cyber, id = 11 } 38 | , { name = "Info1 TP1", slug = "111", collection = Info, id = 12 } 39 | , { name = "Info1 TP2", slug = "121", collection = Info, id = 13 } 40 | , { name = "Info2 TP1", slug = "211", collection = Info, id = 14 } 41 | , { name = "Info2 TP2", slug = "221", collection = Info, id = 15 } 42 | , { name = "Info3 TP1", slug = "311", collection = Info, id = 16 } 43 | , { name = "Info3 TP2", slug = "321", collection = Info, id = 17 } 44 | ] 45 | 46 | 47 | firstGroup : Group 48 | firstGroup = 49 | { name = "Cyber1 TD1 TP1", slug = "111", collection = Cyber, id = 0 } 50 | 51 | 52 | minWeekWidth : Int 53 | minWeekWidth = 54 | 660 55 | -------------------------------------------------------------------------------- /src/Cyberplanning/Cyberplanning.elm: -------------------------------------------------------------------------------- 1 | module Cyberplanning.Cyberplanning exposing (Msg, State, initState, request, restoreState, storeState, update, view) 2 | 3 | import Config exposing (allGroups) 4 | import Cyberplanning.PlanningRequest exposing (maybeCreatePlanningRequest) 5 | import Cyberplanning.Storage exposing (decodeState, encodeState) 6 | import Cyberplanning.Types exposing (CustomEvent(..), FetchStatus(..), InternalMsg(..), InternalState, RequestAction(..), defaultState) 7 | import Cyberplanning.Utils exposing (toCalEvents, toCalEventsWithSource) 8 | import Html exposing (Html, text) 9 | import Http exposing (Error) 10 | import Time exposing (Posix) 11 | 12 | 13 | 14 | -- STATE 15 | 16 | 17 | type alias State = 18 | InternalState 19 | 20 | 21 | type alias Msg = 22 | InternalMsg 23 | 24 | 25 | initState : State 26 | initState = 27 | defaultState 28 | 29 | 30 | 31 | -- UPDATE 32 | 33 | 34 | update : Msg -> State -> ( State, RequestAction ) 35 | update msg state = 36 | case msg of 37 | Noop -> 38 | ( state, NoAction ) 39 | 40 | CheckEvents type_ checked -> 41 | let 42 | s = 43 | state.settings 44 | 45 | settings = 46 | case type_ of 47 | Hack2g2 -> 48 | { s | showHack2g2 = checked } 49 | 50 | Custom -> 51 | { s | showCustom = checked } 52 | 53 | -- action = 54 | -- maybeCreatePlanningRequest model.calendarState.viewing model.selectedGroups settings 55 | -- |> queryReload 56 | in 57 | ( { state | status = Loading, settings = settings }, RequestApi ) 58 | 59 | SetGroups idsStrings -> 60 | let 61 | groupsIds = 62 | List.map (String.toInt >> Maybe.withDefault 0) idsStrings 63 | 64 | groups = 65 | List.filter (\x -> List.member x.id groupsIds) allGroups 66 | 67 | -- action = 68 | -- maybeCreatePlanningRequest model.calendarState.viewing groups state.settings 69 | -- |> queryReload 70 | in 71 | ( { state | selectedGroups = groups, status = Normal }, RequestApi ) 72 | 73 | GraphQlResult response -> 74 | case response of 75 | Ok query -> 76 | let 77 | cyberEvents = 78 | query.planning.events 79 | |> toCalEvents state.selectedGroups 80 | 81 | hack2g2Events = 82 | case query.hack2g2 of 83 | Nothing -> 84 | [] 85 | 86 | Just p -> 87 | p.events 88 | |> toCalEventsWithSource "Hack2g2" "#00ff1d" 89 | 90 | customEvents = 91 | case query.custom of 92 | Nothing -> 93 | [] 94 | 95 | Just p -> 96 | p.events 97 | |> toCalEventsWithSource "Custom" "#d82727" 98 | 99 | allEvents = 100 | cyberEvents 101 | ++ hack2g2Events 102 | ++ customEvents 103 | in 104 | ( { state | events = allEvents, status = Normal, groupsCount = List.length state.selectedGroups }, SaveState ) 105 | 106 | Err err -> 107 | ( { state | status = Error err }, NoAction ) 108 | 109 | 110 | 111 | -- VIEW 112 | 113 | 114 | view : State -> Html Msg 115 | view _ = 116 | text "" 117 | 118 | 119 | 120 | -- INTERFACE 121 | 122 | 123 | request : State -> Posix -> ( State, Cmd Msg ) 124 | request state date = 125 | let 126 | reqAction = 127 | maybeCreatePlanningRequest date state.selectedGroups state.settings 128 | in 129 | ( { state | status = Loading }, reqAction ) 130 | 131 | 132 | 133 | -- STORAGE 134 | 135 | 136 | storeState : State -> String 137 | storeState = 138 | encodeState 139 | 140 | 141 | restoreState : String -> State 142 | restoreState = 143 | decodeState 144 | -------------------------------------------------------------------------------- /src/Cyberplanning/PlanningRequest.elm: -------------------------------------------------------------------------------- 1 | module Cyberplanning.PlanningRequest exposing (createPlanningRequest, maybeCreatePlanningRequest) 2 | 3 | import Cyberplanning.Query exposing (Params, sendRequest) 4 | import Cyberplanning.Types exposing (Collection(..), Group, InternalMsg, Settings) 5 | import Cyberplanning.Utils exposing (toDatetime) 6 | import MyTime 7 | import Time exposing (Posix) 8 | import Time.Extra as TimeExtra 9 | 10 | 11 | maybeCreatePlanningRequest : Posix -> List Group -> Settings -> Cmd InternalMsg 12 | maybeCreatePlanningRequest date groups settings = 13 | let 14 | maybeFirstGrp = 15 | List.head groups 16 | 17 | slugs = 18 | List.map .slug groups 19 | in 20 | case maybeFirstGrp of 21 | Just firstGroup -> 22 | case firstGroup.collection of 23 | Cyber -> 24 | createPlanningRequest date "CYBER" slugs settings 25 | |> sendRequest 26 | 27 | Info -> 28 | createPlanningRequest date "INFO" slugs settings 29 | |> sendRequest 30 | 31 | Nothing -> 32 | Cmd.none 33 | 34 | 35 | createPlanningRequest : Posix -> String -> List String -> Settings -> Params 36 | createPlanningRequest date collectionName slugs settings = 37 | let 38 | dateFrom = 39 | date 40 | |> MyTime.floor TimeExtra.Month 41 | |> MyTime.floor TimeExtra.Monday 42 | |> toDatetime 43 | 44 | dateTo = 45 | date 46 | -- Fix issue : Event not loaded in October to November transition 47 | |> MyTime.add TimeExtra.Day 1 48 | |> MyTime.ceiling TimeExtra.Month 49 | |> MyTime.ceiling TimeExtra.Sunday 50 | |> toDatetime 51 | in 52 | { collec = collectionName 53 | , from = dateFrom 54 | , to = dateTo 55 | , grs = slugs 56 | , hack2g2 = settings.showHack2g2 57 | , custom = settings.showCustom 58 | } 59 | -------------------------------------------------------------------------------- /src/Cyberplanning/Query.elm: -------------------------------------------------------------------------------- 1 | module Cyberplanning.Query exposing (Params, eventsApiQuery, sendRequest) 2 | 3 | import Config 4 | import Cyberplanning.Types exposing (Event, InternalMsg(..), Planning, Query) 5 | import Http exposing (Body, Error, Header, Request) 6 | import Json.Decode as Decode exposing (Decoder, field, maybe, string) 7 | import Json.Encode as Encode 8 | 9 | 10 | eventsApiQuery : String 11 | eventsApiQuery = 12 | """query day_planning($collec: Collection!, $grs: [String], $to: DateTime!, $from: DateTime!, $hack2g2: Boolean!, $custom: Boolean!) { 13 | planning(collection: $collec, affiliationGroups: $grs, toDate: $to, fromDate: $from) { 14 | ...events 15 | } 16 | hack2g2: planning(collection: HACK2G2, toDate: $to, fromDate: $from) @include(if: $hack2g2) { 17 | ...events 18 | } 19 | custom: planning(collection: CUSTOM, affiliationGroups: $grs, toDate: $to, fromDate: $from) @include(if: $custom) { 20 | ...events 21 | } 22 | } 23 | fragment events on Planning { 24 | events { 25 | title 26 | eventId 27 | startDate 28 | endDate 29 | classrooms 30 | teachers 31 | groups 32 | affiliations 33 | } 34 | } 35 | """ 36 | 37 | 38 | type alias Params = 39 | { collec : String 40 | , from : String 41 | , to : String 42 | , grs : List String 43 | , hack2g2 : Bool 44 | , custom : Bool 45 | } 46 | 47 | 48 | post : String -> List Header -> Body -> Request Query 49 | post url headers body = 50 | Http.request 51 | { method = "POST" 52 | , headers = headers 53 | , url = url 54 | , body = body 55 | , expect = Http.expectJson (Decode.field "data" decodeQuery) 56 | , timeout = Nothing 57 | , withCredentials = False 58 | } 59 | 60 | 61 | -- authorizationHeader : String -> Header 62 | -- authorizationHeader = 63 | -- Http.header "Authorization" 64 | 65 | 66 | requestAPI : (Result Error Query -> InternalMsg) -> Request Query -> Cmd InternalMsg 67 | requestAPI = 68 | Http.send 69 | 70 | 71 | requestBody : String -> Params -> Body 72 | requestBody queryString { collec, from, to, grs, hack2g2, custom } = 73 | let 74 | var = 75 | Encode.object 76 | [ ( "collec", Encode.string collec ) 77 | , ( "from", Encode.string from ) 78 | , ( "to", Encode.string to ) 79 | , ( "grs", Encode.list Encode.string grs ) 80 | , ( "hack2g2", Encode.bool hack2g2 ) 81 | , ( "custom", Encode.bool custom ) 82 | ] 83 | in 84 | Encode.object 85 | [ ( "query", Encode.string queryString ) 86 | , ( "variables", var ) 87 | ] 88 | |> Http.jsonBody 89 | 90 | 91 | sendRequest : Params -> Cmd InternalMsg 92 | sendRequest params = 93 | requestBody eventsApiQuery params 94 | |> post Config.apiUrl [] 95 | |> requestAPI GraphQlResult 96 | 97 | 98 | decodePlanning : Decoder Planning 99 | decodePlanning = 100 | Decode.map Planning 101 | (field "events" (Decode.list decodeEvent)) 102 | 103 | 104 | listOrNull : Decoder (Maybe (List String)) 105 | listOrNull = 106 | Decode.string 107 | |> Decode.list 108 | |> Decode.nullable 109 | 110 | 111 | decodeEvent : Decoder Event 112 | decodeEvent = 113 | Decode.map8 Event 114 | (field "title" string) 115 | (field "startDate" string) 116 | (field "endDate" string) 117 | (field "classrooms" listOrNull) 118 | (field "teachers" listOrNull) 119 | (field "groups" listOrNull) 120 | (field "affiliations" listOrNull) 121 | (field "eventId" string) 122 | 123 | 124 | decodeQuery : Decoder Query 125 | decodeQuery = 126 | Decode.map3 Query 127 | (field "planning" decodePlanning) 128 | (Decode.maybe (field "hack2g2" decodePlanning)) 129 | (Decode.maybe (field "custom" decodePlanning)) 130 | -------------------------------------------------------------------------------- /src/Cyberplanning/Storage.elm: -------------------------------------------------------------------------------- 1 | module Cyberplanning.Storage exposing (decodeState, encodeState) 2 | 3 | import Calendar.Event as CalEvent 4 | import Config exposing (firstGroup) 5 | import Cyberplanning.Types exposing (Collection(..), FetchStatus(..), Group, InternalState, Settings, defaultSettings) 6 | import Json.Decode as D 7 | import Json.Encode as E 8 | import Time 9 | 10 | 11 | type alias JsonPosition = 12 | { value : String 13 | , param1 : Int 14 | , param2 : Int 15 | } 16 | 17 | 18 | 19 | -- ENCODE 20 | 21 | 22 | encodeState : InternalState -> String 23 | encodeState state = 24 | encoder state 25 | |> E.encode 0 26 | 27 | 28 | encoder : InternalState -> E.Value 29 | encoder state = 30 | E.object 31 | [ ( "events", E.list encoderEvent state.events ) 32 | , ( "settings", encoderSettings state.settings ) 33 | , ( "selectedGroups", E.list encoderGroup state.selectedGroups ) 34 | ] 35 | 36 | 37 | encoderSettings : Settings -> E.Value 38 | encoderSettings settings = 39 | E.object 40 | [ ( "showHack2g2", E.bool settings.showHack2g2 ) 41 | , ( "showCustom", E.bool settings.showCustom ) 42 | ] 43 | 44 | 45 | encoderGroup : Group -> E.Value 46 | encoderGroup group = 47 | E.object 48 | [ ( "name", E.string group.name ) 49 | , ( "slug", E.string group.slug ) 50 | , ( "collection", encoderCollection group.collection ) 51 | , ( "id", E.int group.id ) 52 | ] 53 | 54 | 55 | encoderCollection : Collection -> E.Value 56 | encoderCollection collection = 57 | case collection of 58 | Cyber -> 59 | E.string "Cyber" 60 | 61 | Info -> 62 | E.string "Info" 63 | 64 | 65 | encoderEvent : CalEvent.Event -> E.Value 66 | encoderEvent event = 67 | E.object 68 | [ ( "toId", E.string event.toId ) 69 | , ( "title", E.string event.title ) 70 | , ( "startTime", E.int (Time.posixToMillis event.startTime) ) 71 | , ( "endTime", E.int (Time.posixToMillis event.endTime) ) 72 | , ( "description", E.list E.string event.description ) 73 | , ( "source", E.string event.source ) 74 | , ( "style", encoderStyle event.style ) 75 | , ( "position", encoderPosition event.position ) 76 | ] 77 | 78 | 79 | encoderStyle : CalEvent.Style -> E.Value 80 | encoderStyle style = 81 | E.object 82 | [ ( "eventColor", E.string style.eventColor ) 83 | , ( "textColor", E.string style.textColor ) 84 | ] 85 | 86 | 87 | encoderPosition : CalEvent.PositionMode -> E.Value 88 | encoderPosition position = 89 | case position of 90 | CalEvent.All -> 91 | E.object 92 | [ ( "value", E.string "All" ) 93 | ] 94 | 95 | CalEvent.Column p1 p2 -> 96 | E.object 97 | [ ( "value", E.string "Column" ) 98 | , ( "param1", E.int p1 ) 99 | , ( "param2", E.int p2 ) 100 | ] 101 | 102 | 103 | 104 | -- DECODER 105 | -- debugError : Result err value -> Result err value 106 | -- debugError res = 107 | -- case res of 108 | -- Ok _ -> 109 | -- res 110 | -- Err erros -> 111 | -- let 112 | -- a = 113 | -- Debug.log "Error" erros 114 | -- in 115 | -- res 116 | 117 | 118 | type alias StorageState = 119 | { events : List CalEvent.Event 120 | , selectedGroups : List Group 121 | , settings : Settings 122 | } 123 | 124 | 125 | defaultState : StorageState 126 | defaultState = 127 | { events = [] 128 | , selectedGroups = [ firstGroup ] 129 | , settings = defaultSettings 130 | } 131 | 132 | 133 | decodeState : String -> InternalState 134 | decodeState value = 135 | let 136 | state = 137 | D.decodeString decoder value 138 | -- |> debugError 139 | |> Result.withDefault defaultState 140 | in 141 | { events = state.events 142 | , selectedGroups = state.selectedGroups 143 | , groupsCount = List.length state.selectedGroups 144 | , status = Normal 145 | , settings = state.settings 146 | } 147 | 148 | 149 | decoder : D.Decoder StorageState 150 | decoder = 151 | D.map3 StorageState 152 | (D.field "events" (D.list decoderEvent)) 153 | (D.field "selectedGroups" (D.list decoderGroup)) 154 | (D.field "settings" decoderSettings) 155 | 156 | 157 | decoderSettings : D.Decoder Settings 158 | decoderSettings = 159 | D.map2 Settings 160 | (D.field "showHack2g2" D.bool) 161 | (D.field "showCustom" D.bool) 162 | 163 | 164 | decoderGroup : D.Decoder Group 165 | decoderGroup = 166 | D.map4 Group 167 | (D.field "name" D.string) 168 | (D.field "slug" D.string) 169 | (D.field "collection" decoderCollection) 170 | (D.field "id" D.int) 171 | 172 | 173 | decoderCollection : D.Decoder Collection 174 | decoderCollection = 175 | D.string 176 | |> D.andThen 177 | (\str -> 178 | case str of 179 | "Cyber" -> 180 | D.succeed Cyber 181 | 182 | "Info" -> 183 | D.succeed Info 184 | 185 | somethingElse -> 186 | D.fail <| "Unknown collection: " ++ somethingElse 187 | ) 188 | 189 | 190 | decoderEvent : D.Decoder CalEvent.Event 191 | decoderEvent = 192 | D.map8 CalEvent.Event 193 | (D.field "toId" D.string) 194 | (D.field "title" D.string) 195 | (D.field "startTime" (D.int |> D.andThen (Time.millisToPosix >> D.succeed))) 196 | (D.field "endTime" (D.int |> D.andThen (Time.millisToPosix >> D.succeed))) 197 | (D.field "description" (D.list D.string)) 198 | (D.field "source" D.string) 199 | (D.field "style" decoderStyle) 200 | (D.field "position" decoderPosition) 201 | 202 | 203 | decoderStyle : D.Decoder CalEvent.Style 204 | decoderStyle = 205 | D.map2 CalEvent.Style 206 | (D.field "eventColor" D.string) 207 | (D.field "textColor" D.string) 208 | 209 | 210 | decoderPosition : D.Decoder CalEvent.PositionMode 211 | decoderPosition = 212 | decoderJsonPosition 213 | |> D.andThen 214 | (\entrie -> 215 | case entrie.value of 216 | "All" -> 217 | D.succeed CalEvent.All 218 | 219 | "Column" -> 220 | D.succeed (CalEvent.Column entrie.param1 entrie.param2) 221 | 222 | somethingElse -> 223 | D.fail <| "Unknown position: " ++ somethingElse 224 | ) 225 | 226 | 227 | decoderJsonPosition : D.Decoder JsonPosition 228 | decoderJsonPosition = 229 | D.map3 JsonPosition 230 | (D.field "value" D.string) 231 | (decoderJsonPositionParam "param1" 0) 232 | (decoderJsonPositionParam "param2" 1) 233 | 234 | 235 | decoderJsonPositionParam : String -> Int -> D.Decoder Int 236 | decoderJsonPositionParam name default = 237 | D.maybe (D.field name D.int) 238 | |> D.andThen (Maybe.withDefault default >> D.succeed) 239 | -------------------------------------------------------------------------------- /src/Cyberplanning/Types.elm: -------------------------------------------------------------------------------- 1 | module Cyberplanning.Types exposing (Collection(..), CustomEvent(..), Event, FetchStatus(..), Group, InternalMsg(..), InternalState, Planning, Query, RequestAction(..), Settings, defaultGroups, defaultSettings, defaultState) 2 | 3 | import Calendar.Event as CalEvent 4 | import Http exposing (Error) 5 | 6 | 7 | type InternalMsg 8 | = Noop 9 | | SetGroups (List String) 10 | | CheckEvents CustomEvent Bool 11 | | GraphQlResult (Result Error Query) 12 | 13 | 14 | type CustomEvent 15 | = Hack2g2 16 | | Custom 17 | 18 | 19 | type alias InternalState = 20 | { events : List CalEvent.Event 21 | , selectedGroups : List Group 22 | , groupsCount : Int 23 | , status : FetchStatus 24 | , settings : Settings 25 | } 26 | 27 | 28 | type alias Settings = 29 | { showHack2g2 : Bool 30 | , showCustom : Bool 31 | } 32 | 33 | 34 | defaultState : InternalState 35 | defaultState = 36 | { events = [] 37 | , selectedGroups = defaultGroups 38 | , groupsCount = 0 39 | , status = Normal 40 | , settings = defaultSettings 41 | } 42 | 43 | 44 | defaultSettings : Settings 45 | defaultSettings = 46 | { showHack2g2 = True 47 | , showCustom = True 48 | } 49 | 50 | 51 | defaultGroups : List Group 52 | defaultGroups = 53 | [] 54 | 55 | 56 | type alias Planning = 57 | { events : List Event 58 | } 59 | 60 | 61 | type alias Event = 62 | { title : String 63 | , startDate : String 64 | , endDate : String 65 | , classrooms : Maybe (List String) 66 | , teachers : Maybe (List String) 67 | , groups : Maybe (List String) 68 | , affiliations : Maybe (List String) 69 | , eventId : String 70 | } 71 | 72 | 73 | type alias Query = 74 | { planning : Planning 75 | , hack2g2 : Maybe Planning 76 | , custom : Maybe Planning 77 | } 78 | 79 | 80 | type alias Group = 81 | { name : String 82 | , slug : String 83 | , collection : Collection 84 | , id : Int 85 | } 86 | 87 | 88 | type Collection 89 | = Cyber 90 | | Info 91 | 92 | 93 | type FetchStatus 94 | = Loading 95 | | Error Error 96 | | Normal 97 | 98 | 99 | type RequestAction 100 | = RequestApi 101 | | SaveState 102 | | NoAction 103 | -------------------------------------------------------------------------------- /src/Cyberplanning/Utils.elm: -------------------------------------------------------------------------------- 1 | module Cyberplanning.Utils exposing (computeStyle, extractTimeIsoString, toCalEvent, toCalEventSource, toCalEvents, toCalEventsWithSource, toDatetime) 2 | 3 | import Calendar.Event as CalEvent 4 | import Calendar.Helpers exposing (computeColor) 5 | import Cyberplanning.Types exposing (Event, Group) 6 | import Iso8601 7 | import Set 8 | import Time exposing (Posix) 9 | 10 | 11 | toCalEvents : List Group -> List Event -> List CalEvent.Event 12 | toCalEvents selectedGroups events = 13 | List.map (toCalEvent selectedGroups) events 14 | 15 | 16 | toCalEventsWithSource : String -> String -> List Event -> List CalEvent.Event 17 | toCalEventsWithSource source color events = 18 | List.map (toCalEventSource source color) events 19 | 20 | 21 | toCalEvent : List Group -> Event -> CalEvent.Event 22 | toCalEvent selectedGroups event = 23 | let 24 | classes = 25 | Maybe.withDefault [] event.classrooms 26 | 27 | teachers = 28 | Maybe.withDefault [] event.teachers 29 | 30 | groups = 31 | Maybe.withDefault [] event.groups 32 | 33 | selectedGroupsSet = 34 | selectedGroups 35 | |> List.map .slug 36 | |> Set.fromList 37 | 38 | affiliations = 39 | event.affiliations 40 | |> Maybe.withDefault [] 41 | |> Set.fromList 42 | |> Set.intersect selectedGroupsSet 43 | |> Set.toList 44 | 45 | description = 46 | List.map (String.join ", ") [ classes, teachers, groups ] 47 | 48 | selectedLen = 49 | List.length selectedGroups 50 | 51 | affiliationLen = 52 | List.length affiliations 53 | 54 | firstAff = 55 | List.head affiliations 56 | |> Maybe.withDefault "11" 57 | 58 | firstAffIndex = 59 | List.map .slug selectedGroups 60 | |> List.indexedMap Tuple.pair 61 | |> find (\x -> Tuple.second x == firstAff) 62 | |> Maybe.withDefault ( 0, "" ) 63 | |> Tuple.first 64 | 65 | position = 66 | if affiliationLen >= selectedLen || affiliationLen == 0 then 67 | CalEvent.All 68 | 69 | else 70 | CalEvent.Column firstAffIndex affiliationLen 71 | in 72 | { toId = event.eventId 73 | , title = event.title 74 | , startTime = extractTimeIsoString event.startDate 75 | , endTime = extractTimeIsoString event.endDate 76 | , description = description 77 | , style = computeStyle event.title 78 | , source = "" 79 | , position = position 80 | } 81 | 82 | 83 | toCalEventSource : String -> String -> Event -> CalEvent.Event 84 | toCalEventSource source color event = 85 | let 86 | classes = 87 | Maybe.withDefault [] event.classrooms 88 | 89 | teachers = 90 | Maybe.withDefault [] event.teachers 91 | 92 | groups = 93 | Maybe.withDefault [] event.groups 94 | 95 | description = 96 | List.map (String.join ", ") [ classes, teachers, groups ] 97 | in 98 | { toId = event.eventId 99 | , title = event.title 100 | , startTime = extractTimeIsoString event.startDate 101 | , endTime = extractTimeIsoString event.endDate 102 | , description = description 103 | , source = source 104 | , style = 105 | { textColor = color 106 | , eventColor = "black" 107 | } 108 | , position = CalEvent.All 109 | } 110 | 111 | 112 | computeStyle : String -> CalEvent.Style 113 | computeStyle val = 114 | { textColor = "white" 115 | , eventColor = computeColor val 116 | } 117 | 118 | 119 | extractTimeIsoString : String -> Posix 120 | extractTimeIsoString dateString = 121 | dateString 122 | ++ ".000Z" 123 | |> Iso8601.toTime 124 | |> Result.withDefault (Time.millisToPosix 0) 125 | 126 | 127 | toDatetime : Posix -> String 128 | toDatetime = 129 | Iso8601.fromTime >> String.dropRight 14 130 | 131 | 132 | find : (a -> Bool) -> List a -> Maybe a 133 | find predicate list = 134 | case list of 135 | [] -> 136 | Nothing 137 | 138 | first :: rest -> 139 | if predicate first then 140 | Just first 141 | 142 | else 143 | find predicate rest 144 | -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Browser.Dom 5 | import Browser.Events 6 | import Json.Decode as Decode 7 | import Model exposing (Model) 8 | import Msg exposing (Msg(..)) 9 | import Storage 10 | import Task 11 | import Update exposing (update) 12 | import Utils exposing (initialModel) 13 | import View.View exposing (view) 14 | 15 | 16 | 17 | ---- PROGRAM ---- 18 | 19 | 20 | init : Storage.Storage -> ( Model, Cmd Msg ) 21 | init storage = 22 | ( initialModel storage, Task.perform WindowSize Browser.Dom.getViewport ) 23 | 24 | 25 | main : Program Storage.Storage Model Msg 26 | main = 27 | Browser.document 28 | { view = view 29 | , init = init 30 | , update = update 31 | , subscriptions = subscriptions 32 | } 33 | 34 | 35 | subscriptions : Model -> Sub Msg 36 | subscriptions _ = 37 | Browser.Events.onKeyDown (Decode.map KeyDown keyDecoder) 38 | 39 | 40 | keyDecoder : Decode.Decoder Int 41 | keyDecoder = 42 | Decode.field "keyCode" Decode.int 43 | -------------------------------------------------------------------------------- /src/Model.elm: -------------------------------------------------------------------------------- 1 | module Model exposing (Model, WindowSize) 2 | 3 | import Calendar.Calendar as Calendar 4 | import Cyberplanning.Cyberplanning as Cyberplanning 5 | import Personnel.Personnel as Personnel 6 | import Secret.Secret 7 | import Time exposing (Posix) 8 | import Vendor.Swipe 9 | 10 | 11 | 12 | ---- MODEL ---- 13 | 14 | 15 | type alias WindowSize = 16 | { width : Int 17 | , height : Int 18 | } 19 | 20 | 21 | type alias Model = 22 | { date : Maybe Posix 23 | , size : WindowSize 24 | , swipe : Vendor.Swipe.State 25 | , loop : Bool 26 | , secret : Secret.Secret.StateList 27 | , tooltipHover : Bool 28 | , menuOpened : Bool 29 | , calendarState : Calendar.State 30 | , planningState : Cyberplanning.State 31 | , personnelState : Personnel.State 32 | } 33 | -------------------------------------------------------------------------------- /src/Msg.elm: -------------------------------------------------------------------------------- 1 | module Msg exposing (Msg(..)) 2 | 3 | import Browser.Dom exposing (Viewport) 4 | import Calendar.Msg as Calendar 5 | import Cyberplanning.Cyberplanning as Cyberplanning 6 | import Personnel.Personnel as Personnel 7 | import Time exposing (Posix) 8 | import Vendor.Swipe 9 | 10 | 11 | 12 | {- Events types 13 | 14 | - Init: 15 | SetDate 16 | WindowSize 17 | 18 | - TopBar: 19 | ClickToday 20 | Reload 21 | ToggleMenu 22 | 23 | - SideMenu: 24 | ChangeMode 25 | 26 | - Internal: 27 | SetCalendarState 28 | SetPersonnelState 29 | SetPlanningState 30 | 31 | - Subscription: 32 | KeyDown 33 | SwipeEvent 34 | StopReloadIcon 35 | 36 | -} 37 | 38 | 39 | type Msg 40 | = Noop 41 | | SetDate Posix 42 | | WindowSize Viewport 43 | | KeyDown Int 44 | | SetCalendarState Calendar.Msg 45 | | SetPersonnelState Personnel.Msg 46 | | SetPlanningState Cyberplanning.Msg 47 | | SwipeEvent Vendor.Swipe.Msg 48 | | ClickToday 49 | | ToggleMenu 50 | | StopReloadIcon () 51 | | ChangeMode Calendar.TimeSpan 52 | | Reload 53 | -------------------------------------------------------------------------------- /src/MyTime.elm: -------------------------------------------------------------------------------- 1 | module MyTime exposing (add, ceiling, diff, floor, partsToPosix, range, toDay, toHour, toMinute, toMonth, toOffset, toSecond, toWeekday, toYear, weekdayToNumber, monthToString) 2 | 3 | import Time exposing (Posix, Weekday(..), Month(..)) 4 | import Time.Extra as TimeExtra exposing (Interval) 5 | import Vendor.TimeZone exposing (europe__paris) 6 | 7 | 8 | toYear : Posix -> Int 9 | toYear = 10 | Time.toYear europe__paris 11 | 12 | 13 | add : Interval -> Int -> Posix -> Posix 14 | add interval value = 15 | TimeExtra.add interval value europe__paris 16 | 17 | 18 | ceiling : Interval -> Posix -> Posix 19 | ceiling interval = 20 | TimeExtra.ceiling interval europe__paris 21 | 22 | 23 | floor : Interval -> Posix -> Posix 24 | floor interval = 25 | TimeExtra.floor interval europe__paris 26 | 27 | 28 | diff : Interval -> Posix -> Posix -> Int 29 | diff interval = 30 | TimeExtra.diff interval europe__paris 31 | 32 | 33 | range : Interval -> Int -> Posix -> Posix -> List Posix 34 | range interval value = 35 | TimeExtra.range interval value europe__paris 36 | 37 | 38 | toOffset : Posix -> Float 39 | toOffset = 40 | TimeExtra.toOffset europe__paris >> toFloat 41 | 42 | 43 | toSecond : Posix -> Int 44 | toSecond = 45 | Time.toSecond europe__paris 46 | 47 | 48 | toMinute : Posix -> Int 49 | toMinute = 50 | Time.toMinute europe__paris 51 | 52 | 53 | toHour : Posix -> Int 54 | toHour = 55 | Time.toHour europe__paris 56 | 57 | 58 | toDay : Posix -> Int 59 | toDay = 60 | Time.toDay europe__paris 61 | 62 | 63 | toWeekday : Posix -> Time.Weekday 64 | toWeekday = 65 | Time.toWeekday europe__paris 66 | 67 | 68 | toMonth : Posix -> Time.Month 69 | toMonth = 70 | Time.toMonth europe__paris 71 | 72 | 73 | partsToPosix : TimeExtra.Parts -> Posix 74 | partsToPosix = 75 | TimeExtra.partsToPosix europe__paris 76 | 77 | 78 | weekdayToNumber : Time.Weekday -> Int 79 | weekdayToNumber wd = 80 | case wd of 81 | Mon -> 82 | 1 83 | 84 | Tue -> 85 | 2 86 | 87 | Wed -> 88 | 3 89 | 90 | Thu -> 91 | 4 92 | 93 | Fri -> 94 | 5 95 | 96 | Sat -> 97 | 6 98 | 99 | Sun -> 100 | 7 101 | 102 | 103 | monthToString : Time.Month -> String 104 | monthToString month = 105 | case month of 106 | Jan -> 107 | "Janvier" 108 | 109 | Feb -> 110 | "Février" 111 | 112 | Mar -> 113 | "Mars" 114 | 115 | Apr -> 116 | "Avril" 117 | 118 | May -> 119 | "Mai" 120 | 121 | Jun -> 122 | "Juin" 123 | 124 | Jul -> 125 | "Juillet" 126 | 127 | Aug -> 128 | "Août" 129 | 130 | Sep -> 131 | "Septembre" 132 | 133 | Oct -> 134 | "Octobre" 135 | 136 | Nov -> 137 | "Novembre" 138 | 139 | Dec -> 140 | "Décembre" -------------------------------------------------------------------------------- /src/Personnel/Ical.elm: -------------------------------------------------------------------------------- 1 | module Personnel.Ical exposing (VCalendar, VEvent, processIcal, emptyVCalendar, emptyVEvent) 2 | 3 | import Array 4 | import Time exposing (Posix) 5 | import Personnel.Timeparser exposing (toTime) 6 | 7 | 8 | type alias VCalendar = 9 | { events : List VEvent 10 | , working : Bool 11 | , parsing : VEvent 12 | } 13 | 14 | 15 | type alias VEvent = 16 | { summary : String 17 | , dtstart : Posix 18 | , dtend : Posix 19 | } 20 | 21 | 22 | epoch : Posix 23 | epoch = 24 | Time.millisToPosix 10000 25 | 26 | 27 | processIcal : String -> List VEvent 28 | processIcal data = 29 | data 30 | |> String.split "\n" 31 | |> parseCalendar 32 | |> .events 33 | 34 | 35 | emptyVCalendar : VCalendar 36 | emptyVCalendar = 37 | { events = [] 38 | , working = False 39 | , parsing = emptyVEvent 40 | } 41 | 42 | 43 | emptyVEvent : VEvent 44 | emptyVEvent = 45 | { summary = "" 46 | , dtstart = epoch 47 | , dtend = epoch 48 | } 49 | 50 | 51 | parseCalendar : List String -> VCalendar 52 | parseCalendar lines = 53 | List.foldl innerParseCalendar emptyVCalendar lines 54 | 55 | 56 | innerParseCalendar : String -> VCalendar -> VCalendar 57 | innerParseCalendar line cal = 58 | let 59 | lineTrim = String.trim line 60 | 61 | parts = 62 | lineTrim 63 | |> String.trim 64 | |> String.split ":" 65 | |> Array.fromList 66 | 67 | key = 68 | Array.get 0 parts 69 | |> Maybe.andThen parseVKey 70 | |> Maybe.withDefault "" 71 | 72 | value = 73 | Array.get 1 parts 74 | |> Maybe.withDefault "" 75 | in 76 | case lineTrim of 77 | "BEGIN:VEVENT" -> 78 | { cal | working = True } 79 | 80 | "END:VEVENT" -> 81 | { cal | working = False, events = cal.parsing :: cal.events, parsing = emptyVEvent } 82 | 83 | _ -> 84 | let 85 | parsing = 86 | cal.parsing 87 | 88 | event = 89 | case key of 90 | "SUMMARY" -> 91 | { parsing | summary = value } 92 | 93 | "DTSTART" -> 94 | { parsing | dtstart = parseCalDatetime value } 95 | 96 | "DTEND" -> 97 | { parsing | dtend = parseCalDatetime value } 98 | 99 | _ -> 100 | parsing 101 | in 102 | { cal | parsing = event } 103 | 104 | 105 | parseCalDatetime : String -> Posix 106 | parseCalDatetime value = 107 | case toTime value of 108 | Ok time -> 109 | time 110 | 111 | Err _ -> 112 | -- let 113 | -- a = 114 | -- Debug.log "Parsing errors" errors 115 | -- b = 116 | -- Debug.log "value" value 117 | -- in 118 | Time.millisToPosix 0 119 | 120 | 121 | parseVKey : String -> Maybe String 122 | parseVKey key = 123 | key 124 | |> String.split ";" 125 | |> List.head 126 | -------------------------------------------------------------------------------- /src/Personnel/Personnel.elm: -------------------------------------------------------------------------------- 1 | module Personnel.Personnel exposing (Msg, State, getEvents, initState, restoreState, storeState, update, view) 2 | 3 | import Calendar.Event as CalEvent 4 | import File exposing (File) 5 | import File.Select as Select 6 | import Html exposing (Html, button, div, input, label, text) 7 | import Html.Attributes exposing (checked, class, for, id, title, type_) 8 | import Html.Events exposing (onCheck, onClick) 9 | import MyTime 10 | import Personnel.Ical exposing (VEvent, processIcal) 11 | import Personnel.Storage exposing (decodeState, encodeState) 12 | import Personnel.Types exposing (FileInfos, InternalState, defaultState) 13 | import Task 14 | 15 | 16 | 17 | -- STATE 18 | 19 | 20 | type alias State = 21 | InternalState 22 | 23 | 24 | initState : State 25 | initState = 26 | defaultState 27 | 28 | 29 | 30 | -- UPDATE 31 | 32 | 33 | type Msg 34 | = FileSelected File 35 | | FileLoaded String 36 | | CheckActive Bool 37 | | RemoveFile 38 | 39 | 40 | update : Msg -> State -> ( State, Cmd Msg ) 41 | update msg state = 42 | case msg of 43 | FileSelected file -> 44 | ( { state | file = Just (extractFileInfos file) } 45 | , Task.perform FileLoaded (File.toString file) 46 | ) 47 | 48 | FileLoaded content -> 49 | ( { state | events = processIcal content |> personnelToEvents } 50 | , Cmd.none 51 | ) 52 | 53 | CheckActive checked -> 54 | let 55 | action = 56 | if checked && state.file == Nothing then 57 | Select.file [ "text/calendar" ] FileSelected 58 | 59 | else 60 | Cmd.none 61 | in 62 | ( { state | active = checked }, action ) 63 | 64 | RemoveFile -> 65 | ( { state | active = False, file = Nothing, events = [] }, Cmd.none ) 66 | 67 | 68 | 69 | -- VIEW 70 | 71 | 72 | view : State -> Html Msg 73 | view state = 74 | div 75 | [] 76 | [ div [ class "md-checkbox" ] 77 | [ input [ id "check-personnel", type_ "checkbox", checked state.active, onCheck CheckActive ] [] 78 | , label [ for "check-personnel" ] [ text "Load ICAL" ] 79 | ] 80 | , maybeViewFileInfos state.file 81 | ] 82 | 83 | 84 | maybeViewFileInfos : Maybe FileInfos -> Html Msg 85 | maybeViewFileInfos maybeFile = 86 | case maybeFile of 87 | Just file -> 88 | let 89 | year = 90 | MyTime.toYear file.lastModified 91 | |> String.fromInt 92 | 93 | month = 94 | MyTime.toMonth file.lastModified 95 | |> MyTime.monthToString 96 | |> String.left 3 97 | 98 | day = 99 | MyTime.toDay file.lastModified 100 | |> String.fromInt 101 | in 102 | div 103 | [ onClick RemoveFile, title "Click to remove", class "personnel--fileinfo" ] 104 | [ div [] [ text (file.name ++ " : " ++ day ++ " " ++ month ++ " " ++ year) ] 105 | , div [] [ button [ class "personnel--fileinfo-remove" ] [ text "Remove" ] ] 106 | ] 107 | 108 | _ -> 109 | text "" 110 | 111 | 112 | 113 | -- UTILS 114 | 115 | 116 | extractFileInfos : File -> FileInfos 117 | extractFileInfos file = 118 | { name = File.name file 119 | , lastModified = File.lastModified file 120 | } 121 | 122 | 123 | personnelToEvents : List VEvent -> List CalEvent.Event 124 | personnelToEvents vevents = 125 | List.map personnelToEvent vevents 126 | 127 | 128 | personnelToEvent : VEvent -> CalEvent.Event 129 | personnelToEvent vevent = 130 | { toId = vevent.summary 131 | , title = vevent.summary 132 | , startTime = vevent.dtstart 133 | , endTime = vevent.dtend 134 | , description = [] 135 | , source = "Personnel" 136 | , style = 137 | { textColor = "orange" 138 | , eventColor = "black" 139 | } 140 | , position = CalEvent.All 141 | } 142 | 143 | 144 | 145 | -- INTERFACE 146 | 147 | 148 | getEvents : State -> List CalEvent.Event 149 | getEvents { active, events } = 150 | if active then 151 | events 152 | 153 | else 154 | [] 155 | 156 | 157 | 158 | -- STORAGE 159 | 160 | 161 | storeState : State -> String 162 | storeState = 163 | encodeState 164 | 165 | 166 | restoreState : String -> State 167 | restoreState = 168 | decodeState 169 | -------------------------------------------------------------------------------- /src/Personnel/Storage.elm: -------------------------------------------------------------------------------- 1 | module Personnel.Storage exposing (decodeState, encodeState) 2 | 3 | import Calendar.Event as CalEvent 4 | import Json.Decode as D 5 | import Json.Encode as E 6 | import Personnel.Types exposing (FileInfos, InternalState, defaultState) 7 | import Time 8 | 9 | 10 | type alias JsonPosition = 11 | { value : String 12 | , param1 : Int 13 | , param2 : Int 14 | } 15 | 16 | 17 | 18 | -- ENCODE 19 | 20 | 21 | encodeState : InternalState -> String 22 | encodeState state = 23 | encoder state 24 | |> E.encode 0 25 | 26 | 27 | encoder : InternalState -> E.Value 28 | encoder state = 29 | E.object 30 | ([ ( "events", E.list encoderEvent state.events ) 31 | , ( "active", E.bool state.active ) 32 | ] 33 | ++ (Maybe.andThen (\x -> Just [ ( "file", encoderFile x ) ]) state.file 34 | |> Maybe.withDefault [] 35 | ) 36 | ) 37 | 38 | 39 | encoderFile : FileInfos -> E.Value 40 | encoderFile file = 41 | E.object 42 | [ ( "name", E.string file.name ) 43 | , ( "lastModified", E.int (Time.posixToMillis file.lastModified) ) 44 | ] 45 | 46 | 47 | encoderEvent : CalEvent.Event -> E.Value 48 | encoderEvent event = 49 | E.object 50 | [ ( "toId", E.string event.toId ) 51 | , ( "title", E.string event.title ) 52 | , ( "startTime", E.int (Time.posixToMillis event.startTime) ) 53 | , ( "endTime", E.int (Time.posixToMillis event.endTime) ) 54 | , ( "description", E.list E.string event.description ) 55 | , ( "source", E.string event.source ) 56 | , ( "style", encoderStyle event.style ) 57 | , ( "position", encoderPosition event.position ) 58 | ] 59 | 60 | 61 | encoderStyle : CalEvent.Style -> E.Value 62 | encoderStyle style = 63 | E.object 64 | [ ( "eventColor", E.string style.eventColor ) 65 | , ( "textColor", E.string style.textColor ) 66 | ] 67 | 68 | 69 | encoderPosition : CalEvent.PositionMode -> E.Value 70 | encoderPosition position = 71 | case position of 72 | CalEvent.All -> 73 | E.object 74 | [ ( "value", E.string "All" ) 75 | ] 76 | 77 | CalEvent.Column p1 p2 -> 78 | E.object 79 | [ ( "value", E.string "Column" ) 80 | , ( "param1", E.int p1 ) 81 | , ( "param2", E.int p2 ) 82 | ] 83 | 84 | 85 | 86 | -- DECODER 87 | -- debugError : Result err value -> Result err value 88 | -- debugError res = 89 | -- case res of 90 | -- Ok _ -> 91 | -- res 92 | -- Err erros -> 93 | -- let 94 | -- a = 95 | -- Debug.log "Error" erros 96 | -- in 97 | -- res 98 | 99 | 100 | decodeState : String -> InternalState 101 | decodeState value = 102 | D.decodeString decoder value 103 | -- |> debugError 104 | |> Result.withDefault defaultState 105 | 106 | 107 | decoder : D.Decoder InternalState 108 | decoder = 109 | D.map3 InternalState 110 | (D.field "events" (D.list decoderEvent)) 111 | (D.maybe (D.field "file" decoderFile)) 112 | (D.field "active" D.bool) 113 | 114 | 115 | decoderFile : D.Decoder FileInfos 116 | decoderFile = 117 | D.map2 FileInfos 118 | (D.field "name" D.string) 119 | (D.field "lastModified" (D.int |> D.andThen (Time.millisToPosix >> D.succeed))) 120 | 121 | 122 | decoderEvent : D.Decoder CalEvent.Event 123 | decoderEvent = 124 | D.map8 CalEvent.Event 125 | (D.field "toId" D.string) 126 | (D.field "title" D.string) 127 | (D.field "startTime" (D.int |> D.andThen (Time.millisToPosix >> D.succeed))) 128 | (D.field "endTime" (D.int |> D.andThen (Time.millisToPosix >> D.succeed))) 129 | (D.field "description" (D.list D.string)) 130 | (D.field "source" D.string) 131 | (D.field "style" decoderStyle) 132 | (D.field "position" decoderPosition) 133 | 134 | 135 | decoderStyle : D.Decoder CalEvent.Style 136 | decoderStyle = 137 | D.map2 CalEvent.Style 138 | (D.field "eventColor" D.string) 139 | (D.field "textColor" D.string) 140 | 141 | 142 | decoderPosition : D.Decoder CalEvent.PositionMode 143 | decoderPosition = 144 | decoderJsonPosition 145 | |> D.andThen 146 | (\entrie -> 147 | case entrie.value of 148 | "All" -> 149 | D.succeed CalEvent.All 150 | 151 | "Column" -> 152 | D.succeed (CalEvent.Column entrie.param1 entrie.param2) 153 | 154 | somethingElse -> 155 | D.fail <| "Unknown position: " ++ somethingElse 156 | ) 157 | 158 | 159 | decoderJsonPosition : D.Decoder JsonPosition 160 | decoderJsonPosition = 161 | D.map3 JsonPosition 162 | (D.field "value" D.string) 163 | (decoderJsonPositionParam "param1" 0) 164 | (decoderJsonPositionParam "param2" 1) 165 | 166 | 167 | decoderJsonPositionParam : String -> Int -> D.Decoder Int 168 | decoderJsonPositionParam name default = 169 | D.maybe (D.field name D.int) 170 | |> D.andThen (Maybe.withDefault default >> D.succeed) 171 | -------------------------------------------------------------------------------- /src/Personnel/Types.elm: -------------------------------------------------------------------------------- 1 | module Personnel.Types exposing (FileInfos, InternalState, defaultState) 2 | 3 | import Calendar.Event as CalEvent 4 | import Time exposing (Posix) 5 | 6 | 7 | type alias FileInfos = 8 | { name : String 9 | , lastModified : Posix 10 | } 11 | 12 | 13 | type alias InternalState = 14 | { events : List CalEvent.Event 15 | , file : Maybe FileInfos 16 | , active : Bool 17 | } 18 | 19 | 20 | defaultState : InternalState 21 | defaultState = 22 | { events = [] 23 | , file = Nothing 24 | , active = False 25 | } 26 | -------------------------------------------------------------------------------- /src/Secret/Help.elm: -------------------------------------------------------------------------------- 1 | module Secret.Help exposing (Help, helpEvents, helpMessages) 2 | 3 | import Calendar.Event as Cal 4 | import Cyberplanning.Utils exposing (computeStyle) 5 | import MyTime 6 | import Time exposing (Posix) 7 | import Time.Extra as TimeExtra 8 | 9 | 10 | type alias Help = 11 | { title : String 12 | , desc : String 13 | , desc2 : String 14 | , weekday : TimeExtra.Interval 15 | , startHour : Int 16 | , startMinute : Int 17 | , endHour : Int 18 | , endMinute : Int 19 | } 20 | 21 | 22 | helpMessages : List Help 23 | helpMessages = 24 | [ { title = "ChickenSong" 25 | , desc = "Taper le Konami code" 26 | , desc2 = "" 27 | , weekday = TimeExtra.Monday 28 | , startHour = 8 29 | , startMinute = 42 30 | , endHour = 13 31 | , endMinute = 55 32 | } 33 | , { title = "Help" 34 | , desc = "Affiche ce message" 35 | , desc2 = "" 36 | , weekday = TimeExtra.Tuesday 37 | , startHour = 12 38 | , startMinute = 30 39 | , endHour = 16 40 | , endMinute = 0 41 | } 42 | , { title = "Soviet National Anthem 31 " 43 | , desc = "reverse(KonamiCode)" 44 | , desc2 = "" 45 | , weekday = TimeExtra.Wednesday 46 | , startHour = 8 47 | , startMinute = 0 48 | , endHour = 11 49 | , endMinute = 15 50 | } 51 | , { title = "Samba 🎉" 52 | , desc = "Type S A M B A" 53 | , desc2 = "" 54 | , weekday = TimeExtra.Thursday 55 | , startHour = 17 56 | , startMinute = 0 57 | , endHour = 19 58 | , endMinute = 0 59 | } 60 | ] 61 | 62 | 63 | helpToEvent : Posix -> Help -> Cal.Event 64 | helpToEvent viewing help = 65 | let 66 | eventDay = 67 | MyTime.floor TimeExtra.Monday viewing 68 | |> MyTime.ceiling help.weekday 69 | 70 | start = 71 | MyTime.add TimeExtra.Hour help.startHour eventDay 72 | |> MyTime.add TimeExtra.Minute help.startMinute 73 | 74 | end = 75 | MyTime.add TimeExtra.Hour help.endHour eventDay 76 | |> MyTime.add TimeExtra.Minute help.endMinute 77 | in 78 | { toId = help.title 79 | , title = help.title 80 | , startTime = start 81 | , endTime = end 82 | , description = [ help.desc, help.desc2 ] 83 | , style = computeStyle help.title 84 | , source = "" 85 | , position = Cal.All 86 | } 87 | 88 | 89 | helpEvents : Posix -> List Cal.Event 90 | helpEvents viewing = 91 | List.map (helpToEvent viewing) helpMessages 92 | -------------------------------------------------------------------------------- /src/Secret/Secret.elm: -------------------------------------------------------------------------------- 1 | module Secret.Secret exposing (StateList, classStyle, createStates, isHelpActivated, update, view) 2 | 3 | import Array exposing (Array) 4 | import Dict exposing (Dict) 5 | import Html exposing (Html, iframe) 6 | import Html.Attributes exposing (attribute, class, height, src, style, width) 7 | 8 | 9 | type alias StateList = 10 | { secrets : List YTState 11 | , help : State 12 | } 13 | 14 | 15 | type alias YTState = 16 | { code : Array Int 17 | , index : Int 18 | , yt : String 19 | , opts : Dict String String 20 | , class : String 21 | } 22 | 23 | 24 | type alias State = 25 | { code : Array Int 26 | , index : Int 27 | } 28 | 29 | 30 | createStates : StateList 31 | createStates = 32 | { secrets = 33 | [ { code = Array.fromList [ 38, 38, 40, 40, 37, 39, 37, 39, 66, 65 ] 34 | , index = 0 35 | , yt = "-iYBIsLFbKo" 36 | , opts = 37 | Dict.fromList 38 | [ ( "start", "6" ) 39 | ] 40 | , class = "fun" 41 | } 42 | , { code = Array.fromList [ 65, 66, 39, 37, 39, 37, 40, 40, 38, 38 ] 43 | , index = 0 44 | , yt = "Rm6q_3WGy9M" 45 | , opts = Dict.empty 46 | , class = "fun2" 47 | } 48 | , { code = Array.fromList [ 83, 65, 77, 66, 65 ] 49 | , index = 0 50 | , yt = "HAiHEQblKeQ" 51 | , opts = 52 | Dict.fromList 53 | [ ( "start", "24" ) 54 | ] 55 | , class = "fun3" 56 | } 57 | ] 58 | , help = 59 | { code = Array.fromList [ 72, 69, 76, 80 ] 60 | , index = 0 61 | } 62 | } 63 | 64 | 65 | update : Int -> StateList -> StateList 66 | update key state = 67 | let 68 | secrets = 69 | List.map (updateYTState key) state.secrets 70 | 71 | help = 72 | updateState key state.help 73 | in 74 | { secrets = secrets, help = help } 75 | 76 | 77 | updateState : Int -> State -> State 78 | updateState code state = 79 | let 80 | currentCode = 81 | Array.get state.index state.code 82 | in 83 | case currentCode of 84 | Just expected -> 85 | if code == expected then 86 | { state | index = state.index + 1 } 87 | 88 | else 89 | { state | index = 0 } 90 | 91 | Nothing -> 92 | { state | index = 0 } 93 | 94 | 95 | updateYTState : Int -> YTState -> YTState 96 | updateYTState code state = 97 | let 98 | updated = 99 | updateState code { index = state.index, code = state.code } 100 | in 101 | { code = updated.code 102 | , index = updated.index 103 | , yt = state.yt 104 | , opts = state.opts 105 | , class = state.class 106 | } 107 | 108 | 109 | classStyle : StateList -> List (Html.Attribute msg) 110 | classStyle state = 111 | let 112 | helpStyle = 113 | if isHelpActivated state then 114 | [ class "fun-help" ] 115 | 116 | else 117 | [] 118 | in 119 | List.filter activated state.secrets 120 | |> List.map (.class >> class) 121 | |> (++) helpStyle 122 | 123 | 124 | view : StateList -> Html msg 125 | view state = 126 | List.filter activated state.secrets 127 | |> List.map viewState 128 | |> Html.div [ style "height" "0" ] 129 | 130 | 131 | viewState : YTState -> Html msg 132 | viewState state = 133 | let 134 | extraOpts = 135 | Dict.foldl (\k v s -> (k ++ "=" ++ v) :: s) [] state.opts 136 | |> String.join "&" 137 | 138 | id = 139 | state.yt 140 | in 141 | iframe 142 | [ width 0 143 | , height 0 144 | , src ("https://www.youtube.com/embed/" ++ id ++ "?rel=0&controls=0&showinfo=0&autoplay=1&" ++ extraOpts) 145 | , attribute "frameborder" "0" 146 | , attribute "allow" "autoplay; encrypted-media" 147 | , attribute "allowfullscreen" "1" 148 | ] 149 | [] 150 | 151 | 152 | activated : YTState -> Bool 153 | activated state = 154 | Array.length state.code == state.index 155 | 156 | 157 | isHelpActivated : StateList -> Bool 158 | isHelpActivated { help } = 159 | Array.length help.code == help.index 160 | -------------------------------------------------------------------------------- /src/Storage.elm: -------------------------------------------------------------------------------- 1 | port module Storage exposing (Storage, saveState) 2 | 3 | 4 | type alias Storage = 5 | { graphqlUrl : String 6 | , cyberplanning : String 7 | , personnel : String 8 | } 9 | 10 | 11 | port saveState : ( String, String ) -> Cmd msg 12 | -------------------------------------------------------------------------------- /src/Update.elm: -------------------------------------------------------------------------------- 1 | module Update exposing (update) 2 | 3 | import Calendar.Calendar as Calendar 4 | import Calendar.Msg as CalMsg exposing (TimeSpan(..)) 5 | import Config 6 | import Cyberplanning.Cyberplanning as Cyberplanning 7 | import Cyberplanning.Types exposing (RequestAction(..)) 8 | import Model exposing (Model) 9 | import Msg exposing (Msg(..)) 10 | import MyTime 11 | import Personnel.Personnel as Personnel 12 | import Process 13 | import Secret.Secret as Secret 14 | import Storage 15 | import Task 16 | import Time 17 | import Vendor.Swipe as Swipe 18 | 19 | 20 | 21 | ---- UPDATE ---- 22 | 23 | 24 | update : Msg -> Model -> ( Model, Cmd Msg ) 25 | update msgSource model = 26 | (case msgSource of 27 | Noop -> 28 | ( model, Cmd.none ) 29 | 30 | SetDate date -> 31 | let 32 | timespan = 33 | if model.size.width < Config.minWeekWidth then 34 | Day 35 | 36 | else 37 | Week 38 | 39 | calendar = 40 | Calendar.init timespan date 41 | 42 | ( planning, cmd ) = 43 | Cyberplanning.request model.planningState calendar.viewing 44 | |> updateWith SetPlanningState 45 | in 46 | ( { model | date = Just date, calendarState = calendar, planningState = planning } 47 | , cmd 48 | ) 49 | 50 | Reload -> 51 | let 52 | ( planning, action ) = 53 | Cyberplanning.request model.planningState model.calendarState.viewing 54 | in 55 | ( { model | planningState = planning }, Cmd.map SetPlanningState action ) 56 | 57 | SetCalendarState calendarMsg -> 58 | calendarAction model calendarMsg 59 | 60 | SetPersonnelState personnelMsg -> 61 | if Config.enablePersonnelCal then 62 | personnelAction model personnelMsg 63 | else 64 | ( model , Cmd.none ) 65 | 66 | SetPlanningState personnelMsg -> 67 | let 68 | ( planning, action ) = 69 | Cyberplanning.update personnelMsg model.planningState 70 | 71 | ( planning2, cmd ) = 72 | case action of 73 | RequestApi -> 74 | Cyberplanning.request planning model.calendarState.viewing 75 | |> updateWith SetPlanningState 76 | 77 | SaveState -> 78 | ( planning, Storage.saveState ( "cyberplanning", Cyberplanning.storeState planning ) ) 79 | 80 | NoAction -> 81 | ( planning, Cmd.none ) 82 | in 83 | ( { model | planningState = planning2 }, cmd ) 84 | 85 | WindowSize view -> 86 | ( { model | size = { width = floor view.viewport.width, height = floor view.viewport.height } }, Task.perform SetDate Time.now ) 87 | 88 | KeyDown code -> 89 | let 90 | ( calendarModel, cmd ) = 91 | case code of 92 | 39 -> 93 | calendarAction model CalMsg.PageForward 94 | 95 | 37 -> 96 | calendarAction model CalMsg.PageBack 97 | 98 | _ -> 99 | ( model, Cmd.none ) 100 | 101 | secret = 102 | if Config.enableEasterEgg then 103 | Secret.update code model.secret 104 | else 105 | model.secret 106 | 107 | updatedModel = 108 | { calendarModel | secret = secret } 109 | in 110 | ( updatedModel, cmd ) 111 | 112 | SwipeEvent msg -> 113 | let 114 | updatedSwipe = 115 | Swipe.update msg model.swipe 116 | 117 | action = 118 | case Swipe.hasSwiped updatedSwipe 70 of 119 | Just Swipe.Left -> 120 | Task.succeed (SetCalendarState CalMsg.PageForward) 121 | |> Task.perform identity 122 | 123 | Just Swipe.Right -> 124 | Task.succeed (SetCalendarState CalMsg.PageBack) 125 | |> Task.perform identity 126 | 127 | _ -> 128 | Cmd.none 129 | in 130 | ( { model | swipe = updatedSwipe }, action ) 131 | 132 | ClickToday -> 133 | model.date 134 | |> Maybe.withDefault (Time.millisToPosix 0) 135 | |> CalMsg.ChangeViewing 136 | |> calendarAction model 137 | 138 | StopReloadIcon _ -> 139 | ( { model | loop = False }, Cmd.none ) 140 | 141 | ToggleMenu -> 142 | ( { model | menuOpened = not model.menuOpened }, Cmd.none ) 143 | 144 | ChangeMode mode -> 145 | calendarAction model (CalMsg.ChangeTimeSpan mode) 146 | ) 147 | |> queryReload model 148 | 149 | 150 | calendarAction : Model -> CalMsg.Msg -> ( Model, Cmd Msg ) 151 | calendarAction model calMsg = 152 | let 153 | updatedCalendar = 154 | Calendar.update calMsg model.calendarState 155 | 156 | ( planning, cmd ) = 157 | if MyTime.toMonth updatedCalendar.viewing /= MyTime.toMonth model.calendarState.viewing then 158 | Cyberplanning.request model.planningState updatedCalendar.viewing 159 | |> updateWith SetPlanningState 160 | 161 | else 162 | ( model.planningState, Cmd.none ) 163 | 164 | updatedCalWithJourFerie = 165 | if MyTime.toYear updatedCalendar.viewing /= MyTime.toYear model.calendarState.viewing then 166 | Calendar.init updatedCalendar.timeSpan updatedCalendar.viewing 167 | 168 | else 169 | updatedCalendar 170 | in 171 | ( { model | calendarState = updatedCalWithJourFerie, planningState = planning }, cmd ) 172 | 173 | 174 | personnelAction : Model -> Personnel.Msg -> ( Model, Cmd Msg ) 175 | personnelAction model personnelMsg = 176 | let 177 | ( personnel, action ) = 178 | Personnel.update personnelMsg model.personnelState 179 | 180 | cmd = 181 | Cmd.batch 182 | [ Cmd.map SetPersonnelState action 183 | , Storage.saveState ( "personnel", Personnel.storeState personnel ) 184 | ] 185 | in 186 | ( { model | personnelState = personnel }, cmd ) 187 | 188 | 189 | 190 | updateWith : (subMsg -> Msg) -> ( subModel, Cmd subMsg ) -> ( subModel, Cmd Msg ) 191 | updateWith toMsg ( subModel, subCmd ) = 192 | ( subModel 193 | , Cmd.map toMsg subCmd 194 | ) 195 | 196 | 197 | queryReload : Model -> ( Model, Cmd.Cmd Msg ) -> ( Model, Cmd.Cmd Msg ) 198 | queryReload previousModel ( model, action ) = 199 | if model.planningState.status == Cyberplanning.Types.Loading && previousModel.planningState.status /= Cyberplanning.Types.Loading then 200 | ( { model | loop = True } 201 | , Cmd.batch 202 | [ action 203 | , Process.sleep (1 * 1000) 204 | |> Task.perform StopReloadIcon 205 | ] 206 | ) 207 | 208 | else 209 | ( model, action ) 210 | -------------------------------------------------------------------------------- /src/Utils.elm: -------------------------------------------------------------------------------- 1 | module Utils exposing (initialModel) 2 | 3 | import Calendar.Calendar as Calendar 4 | import Calendar.Msg 5 | import Cyberplanning.Cyberplanning as Cyberplanning 6 | import Model exposing (Model) 7 | import Personnel.Personnel as Personnel 8 | import Secret.Secret as Secret 9 | import Storage 10 | import Time 11 | import Vendor.Swipe 12 | 13 | 14 | initialModel : Storage.Storage -> Model 15 | initialModel { cyberplanning, personnel } = 16 | { date = Nothing 17 | , calendarState = Calendar.init Calendar.Msg.Week (Time.millisToPosix 0) 18 | , size = { width = 1200, height = 800 } 19 | , swipe = Vendor.Swipe.init 20 | , loop = False 21 | , secret = Secret.createStates 22 | , tooltipHover = False 23 | , menuOpened = False 24 | , personnelState = Personnel.restoreState personnel 25 | , planningState = Cyberplanning.restoreState cyberplanning 26 | } 27 | -------------------------------------------------------------------------------- /src/Vendor/Color.elm: -------------------------------------------------------------------------------- 1 | module Vendor.Color exposing 2 | ( Color 3 | , rgb, rgba, hsl, hsla, grayscale, greyscale, complement 4 | , toRgb, toHsl 5 | , black, blue, brown, charcoal, darkBlue, darkBrown, darkCharcoal, darkGray 6 | , darkGreen, darkGrey, darkOrange, darkPurple, darkRed, darkYellow, gray 7 | , green, grey, lightBlue, lightBrown, lightCharcoal, lightGray, lightGreen 8 | , lightGrey, lightOrange, lightPurple, lightRed, lightYellow, orange, purple 9 | , red, white, yellow 10 | ) 11 | 12 | {-| This module provides a simple way of describing colors as RGB with alpha transparency, based on this simple data structure: 13 | 14 | type alias Color = 15 | { red : Int, green : Int, blue : Int, alpha : Float } 16 | 17 | The intention here is to provide a minimal and convenient representation of color for rendering purposes. 18 | 19 | 20 | # The color representations: 21 | 22 | @docs Color 23 | 24 | 25 | # Constructors: 26 | 27 | @docs rgb, rgba, hsl, hsla, grayscale, greyscale, complement 28 | 29 | 30 | # Color space conversion/extraction: 31 | 32 | @docs toRgb, toHsl 33 | 34 | 35 | # Some basic colors to get you started: 36 | 37 | @docs black, blue, brown, charcoal, darkBlue, darkBrown, darkCharcoal, darkGray 38 | @docs darkGreen, darkGrey, darkOrange, darkPurple, darkRed, darkYellow, gray 39 | @docs green, grey, lightBlue, lightBrown, lightCharcoal, lightGray, lightGreen 40 | @docs lightGrey, lightOrange, lightPurple, lightRed, lightYellow, orange, purple 41 | @docs red, white, yellow 42 | 43 | -} 44 | 45 | -- Public API 46 | 47 | 48 | {-| A description of a color as computers see them. 49 | -} 50 | type alias Color = 51 | { red : Int 52 | , green : Int 53 | , blue : Int 54 | , alpha : Float 55 | } 56 | 57 | 58 | {-| Builds an RGBA color from all of its components. 59 | -} 60 | rgba : Int -> Int -> Int -> Float -> Color 61 | rgba = 62 | Color 63 | 64 | 65 | {-| Builds an RGBA color from its RGB components at 100% opacity. 66 | -} 67 | rgb : Int -> Int -> Int -> Color 68 | rgb r g b = 69 | { red = r, green = g, blue = b, alpha = 1 } 70 | 71 | 72 | {-| Builds and RGBA color from its hue, saturation, lighness and alpha (HSLA) representation. 73 | -} 74 | hsla : Float -> Float -> Float -> Float -> Color 75 | hsla hue saturation lightness alpha = 76 | let 77 | ( r, g, b ) = 78 | hslToRgb (hue - turns (toFloat (floor (hue / (2 * pi))))) saturation lightness 79 | in 80 | { red = round (255 * r), green = round (255 * g), blue = round (255 * b), alpha = alpha } 81 | 82 | 83 | {-| Builds and RGBA color from its hue, saturation and lighness (HSL) representation at 100% opacity. 84 | -} 85 | hsl : Float -> Float -> Float -> Color 86 | hsl hue saturation lightness = 87 | hsla hue saturation lightness 1 88 | 89 | 90 | {-| Makes a grey level from 0 to 1. 91 | -} 92 | grayscale : Float -> Color 93 | grayscale p = 94 | hsla 0 0 (1 - p) 1 95 | 96 | 97 | {-| Makes a grey level from 0 to 1. 98 | -} 99 | greyscale : Float -> Color 100 | greyscale p = 101 | hsla 0 0 (1 - p) 1 102 | 103 | 104 | {-| Forms the complement of a color. 105 | -} 106 | complement : Color -> Color 107 | complement color = 108 | let 109 | ( h, s, l ) = 110 | rgbToHsl color.red color.green color.blue 111 | in 112 | hsla (h + degrees 180) s l color.alpha 113 | 114 | 115 | {-| Converts the RGBA color to its HSLA representation. 116 | -} 117 | toHsl : Color -> { hue : Float, saturation : Float, lightness : Float, alpha : Float } 118 | toHsl color = 119 | let 120 | ( h, s, l ) = 121 | rgbToHsl color.red color.green color.blue 122 | in 123 | { hue = h, saturation = s, lightness = l, alpha = color.alpha } 124 | 125 | 126 | {-| Converts the RGBA color to its RGBA representation - that is, does nothing. 127 | -} 128 | toRgb : Color -> { red : Int, green : Int, blue : Int, alpha : Float } 129 | toRgb = 130 | identity 131 | 132 | 133 | 134 | -- Helper functions for converting color spaces. 135 | 136 | 137 | fmod : Float -> Int -> Float 138 | fmod f n = 139 | let 140 | integer = 141 | floor f 142 | in 143 | toFloat (modBy n integer) + f - toFloat integer 144 | 145 | 146 | rgbToHsl : Int -> Int -> Int -> ( Float, Float, Float ) 147 | rgbToHsl redI greenI blueI = 148 | let 149 | r = 150 | toFloat redI / 255 151 | 152 | g = 153 | toFloat greenI / 255 154 | 155 | b = 156 | toFloat blueI / 255 157 | 158 | cMax = 159 | max (max r g) b 160 | 161 | cMin = 162 | min (min r g) b 163 | 164 | c = 165 | cMax - cMin 166 | 167 | hue = 168 | degrees 60 169 | * (if cMax == r then 170 | fmod ((g - b) / c) 6 171 | 172 | else if cMax == g then 173 | ((b - r) / c) + 2 174 | 175 | else 176 | {- cMax == b -} 177 | ((r - g) / c) + 4 178 | ) 179 | 180 | lightness = 181 | (cMax + cMin) / 2 182 | 183 | saturation = 184 | if lightness == 0 then 185 | 0 186 | 187 | else 188 | c / (1 - abs (2 * lightness - 1)) 189 | in 190 | ( hue, saturation, lightness ) 191 | 192 | 193 | hslToRgb : Float -> Float -> Float -> ( Float, Float, Float ) 194 | hslToRgb hue saturation lightness = 195 | let 196 | chroma = 197 | (1 - abs (2 * lightness - 1)) * saturation 198 | 199 | normHue = 200 | hue / degrees 60 201 | 202 | x = 203 | chroma * (1 - abs (fmod normHue 2 - 1)) 204 | 205 | ( r, g, b ) = 206 | if normHue < 0 then 207 | ( 0, 0, 0 ) 208 | 209 | else if normHue < 1 then 210 | ( chroma, x, 0 ) 211 | 212 | else if normHue < 2 then 213 | ( x, chroma, 0 ) 214 | 215 | else if normHue < 3 then 216 | ( 0, chroma, x ) 217 | 218 | else if normHue < 4 then 219 | ( 0, x, chroma ) 220 | 221 | else if normHue < 5 then 222 | ( x, 0, chroma ) 223 | 224 | else if normHue < 6 then 225 | ( chroma, 0, x ) 226 | 227 | else 228 | ( 0, 0, 0 ) 229 | 230 | m = 231 | lightness - chroma / 2 232 | in 233 | ( r + m, g + m, b + m ) 234 | 235 | 236 | 237 | -- Some ready made colors to get you going. 238 | 239 | 240 | {-| -} 241 | lightRed : Color 242 | lightRed = 243 | rgba 239 41 41 1 244 | 245 | 246 | {-| -} 247 | red : Color 248 | red = 249 | rgba 204 0 0 1 250 | 251 | 252 | {-| -} 253 | darkRed : Color 254 | darkRed = 255 | rgba 164 0 0 1 256 | 257 | 258 | {-| -} 259 | lightOrange : Color 260 | lightOrange = 261 | rgba 252 175 62 1 262 | 263 | 264 | {-| -} 265 | orange : Color 266 | orange = 267 | rgba 245 121 0 1 268 | 269 | 270 | {-| -} 271 | darkOrange : Color 272 | darkOrange = 273 | rgba 206 92 0 1 274 | 275 | 276 | {-| -} 277 | lightYellow : Color 278 | lightYellow = 279 | rgba 255 233 79 1 280 | 281 | 282 | {-| -} 283 | yellow : Color 284 | yellow = 285 | rgba 237 212 0 1 286 | 287 | 288 | {-| -} 289 | darkYellow : Color 290 | darkYellow = 291 | rgba 196 160 0 1 292 | 293 | 294 | {-| -} 295 | lightGreen : Color 296 | lightGreen = 297 | rgba 138 226 52 1 298 | 299 | 300 | {-| -} 301 | green : Color 302 | green = 303 | rgba 115 210 22 1 304 | 305 | 306 | {-| -} 307 | darkGreen : Color 308 | darkGreen = 309 | rgba 78 154 6 1 310 | 311 | 312 | {-| -} 313 | lightBlue : Color 314 | lightBlue = 315 | rgba 114 159 207 1 316 | 317 | 318 | {-| -} 319 | blue : Color 320 | blue = 321 | rgba 52 101 164 1 322 | 323 | 324 | {-| -} 325 | darkBlue : Color 326 | darkBlue = 327 | rgba 32 74 135 1 328 | 329 | 330 | {-| -} 331 | lightPurple : Color 332 | lightPurple = 333 | rgba 173 127 168 1 334 | 335 | 336 | {-| -} 337 | purple : Color 338 | purple = 339 | rgba 117 80 123 1 340 | 341 | 342 | {-| -} 343 | darkPurple : Color 344 | darkPurple = 345 | rgba 92 53 102 1 346 | 347 | 348 | {-| -} 349 | lightBrown : Color 350 | lightBrown = 351 | rgba 233 185 110 1 352 | 353 | 354 | {-| -} 355 | brown : Color 356 | brown = 357 | rgba 193 125 17 1 358 | 359 | 360 | {-| -} 361 | darkBrown : Color 362 | darkBrown = 363 | rgba 143 89 2 1 364 | 365 | 366 | {-| -} 367 | black : Color 368 | black = 369 | rgba 0 0 0 1 370 | 371 | 372 | {-| -} 373 | white : Color 374 | white = 375 | rgba 255 255 255 1 376 | 377 | 378 | {-| -} 379 | lightGrey : Color 380 | lightGrey = 381 | rgba 238 238 236 1 382 | 383 | 384 | {-| -} 385 | grey : Color 386 | grey = 387 | rgba 211 215 207 1 388 | 389 | 390 | {-| -} 391 | darkGrey : Color 392 | darkGrey = 393 | rgba 186 189 182 1 394 | 395 | 396 | {-| -} 397 | lightGray : Color 398 | lightGray = 399 | rgba 238 238 236 1 400 | 401 | 402 | {-| -} 403 | gray : Color 404 | gray = 405 | rgba 211 215 207 1 406 | 407 | 408 | {-| -} 409 | darkGray : Color 410 | darkGray = 411 | rgba 186 189 182 1 412 | 413 | 414 | {-| -} 415 | lightCharcoal : Color 416 | lightCharcoal = 417 | rgba 136 138 133 1 418 | 419 | 420 | {-| -} 421 | charcoal : Color 422 | charcoal = 423 | rgba 85 87 83 1 424 | 425 | 426 | {-| -} 427 | darkCharcoal : Color 428 | darkCharcoal = 429 | rgba 46 52 54 1 430 | -------------------------------------------------------------------------------- /src/Vendor/Swipe.elm: -------------------------------------------------------------------------------- 1 | module Vendor.Swipe exposing 2 | ( Coordinates 3 | , Direction(..) 4 | , Msg 5 | , State 6 | , SwipeState(..) 7 | , hasSwiped 8 | , init 9 | , onSwipe 10 | , update 11 | ) 12 | 13 | import Html 14 | import Html.Events as Events 15 | import Json.Decode as Decode exposing (Decoder) 16 | 17 | 18 | type Msg 19 | = Start Touch 20 | | Move Touch 21 | | End Touch 22 | | Cancel Touch 23 | 24 | 25 | type alias State = 26 | { c0 : Coordinates 27 | , c1 : Coordinates 28 | , id : Int 29 | , direction : Maybe Direction 30 | , state : SwipeState 31 | } 32 | 33 | 34 | type SwipeState 35 | = SwipeStart 36 | | Swiping 37 | | SwipeEnd 38 | 39 | 40 | type alias Touch = 41 | { identifier : Int 42 | , coordinates : Coordinates 43 | } 44 | 45 | 46 | type alias Coordinates = 47 | { clientX : Float 48 | , clientY : Float 49 | } 50 | 51 | 52 | type Direction 53 | = Left 54 | | Right 55 | 56 | 57 | init : State 58 | init = 59 | { c0 = emptyCoordinates 60 | , c1 = emptyCoordinates 61 | , id = 0 62 | , direction = Nothing 63 | , state = SwipeEnd 64 | } 65 | 66 | 67 | update : Msg -> State -> State 68 | update msg state = 69 | case msg of 70 | Start touch -> 71 | { c0 = touch.coordinates 72 | , c1 = emptyCoordinates 73 | , id = touch.identifier 74 | , direction = Nothing 75 | , state = SwipeStart 76 | } 77 | 78 | Move touch -> 79 | let 80 | dir = 81 | direction <| subCoordinates touch.coordinates state.c0 82 | in 83 | { state | direction = dir, c1 = touch.coordinates, id = touch.identifier, state = Swiping } 84 | 85 | End touch -> 86 | let 87 | dir = 88 | direction <| subCoordinates touch.coordinates state.c0 89 | in 90 | { state | direction = dir, c1 = touch.coordinates, id = touch.identifier, state = SwipeEnd } 91 | 92 | Cancel touch -> 93 | { c0 = touch.coordinates 94 | , c1 = emptyCoordinates 95 | , id = touch.identifier 96 | , direction = Nothing 97 | , state = SwipeEnd 98 | } 99 | 100 | 101 | hasSwiped : State -> Float -> Maybe Direction 102 | hasSwiped state distance = 103 | if (state.state == SwipeEnd) && (distanceX state.c0 state.c1 > distance) then 104 | state.direction 105 | 106 | else 107 | Nothing 108 | 109 | 110 | direction : Coordinates -> Maybe Direction 111 | direction { clientX, clientY } = 112 | if clientX > 0 then 113 | Just Right 114 | 115 | else if clientX < 0 then 116 | Just Left 117 | 118 | else 119 | Nothing 120 | 121 | 122 | distanceX : Coordinates -> Coordinates -> Float 123 | distanceX c0 c1 = 124 | abs (c0.clientX - c1.clientX) 125 | 126 | 127 | subCoordinates : Coordinates -> Coordinates -> Coordinates 128 | subCoordinates a b = 129 | { clientX = a.clientX - b.clientX 130 | , clientY = a.clientY - b.clientY 131 | } 132 | 133 | 134 | emptyCoordinates : Coordinates 135 | emptyCoordinates = 136 | { clientX = 0.0 137 | , clientY = 0.0 138 | } 139 | 140 | 141 | 142 | -- TOUCH EVENTS ################################################## 143 | 144 | 145 | onSwipe : (Msg -> msg) -> List (Html.Attribute msg) 146 | onSwipe tag = 147 | [ onStart Start tag 148 | 149 | -- , onMove Move tag 150 | , onEnd End tag 151 | 152 | -- , onCancel Cancel tag 153 | ] 154 | 155 | 156 | onStart : (Touch -> Msg) -> (Msg -> msg) -> Html.Attribute msg 157 | onStart tag = 158 | on "touchstart" tag 159 | 160 | 161 | onMove : (Touch -> Msg) -> (Msg -> msg) -> Html.Attribute msg 162 | onMove tag = 163 | on "touchmove" tag 164 | 165 | 166 | onEnd : (Touch -> Msg) -> (Msg -> msg) -> Html.Attribute msg 167 | onEnd tag = 168 | on "touchend" tag 169 | 170 | 171 | onCancel : (Touch -> Msg) -> (Msg -> msg) -> Html.Attribute msg 172 | onCancel tag = 173 | on "touchcancel" tag 174 | 175 | 176 | 177 | -- HELPER FUNCTIONS ################################################## 178 | -- stopOptions : Events.Options 179 | -- stopOptions = 180 | -- { stopPropagation = False 181 | -- , preventDefault = False 182 | -- } 183 | 184 | 185 | on : String -> (Touch -> Msg) -> (Msg -> msg) -> Html.Attribute msg 186 | on event msg tag = 187 | -- Decoder Msg 188 | Decode.map msg decodeCoordinates 189 | -- Decoder msg 190 | |> Decode.map tag 191 | -- Decoder (msg, Bool) 192 | |> Decode.map (\m -> ( m, True )) 193 | |> Events.stopPropagationOn event 194 | 195 | 196 | decodeCoordinates : Decoder Touch 197 | decodeCoordinates = 198 | decode 199 | |> Decode.at [ "changedTouches", "0" ] 200 | 201 | 202 | decode : Decoder Touch 203 | decode = 204 | Decode.map2 Touch 205 | (Decode.field "identifier" Decode.int) 206 | (Decode.map2 Coordinates 207 | (Decode.field "clientX" Decode.float) 208 | (Decode.field "clientY" Decode.float) 209 | ) 210 | -------------------------------------------------------------------------------- /src/Vendor/TimeZone.elm: -------------------------------------------------------------------------------- 1 | module Vendor.TimeZone exposing (europe__paris) 2 | 3 | import Time exposing (Zone, customZone) 4 | 5 | 6 | europe__paris : Zone 7 | europe__paris = 8 | customZone 9 | 60 10 | [ { offset = 60, start = 35667420 } 11 | , { offset = 120, start = 35365020 } 12 | , { offset = 60, start = 35143260 } 13 | , { offset = 120, start = 34840860 } 14 | , { offset = 60, start = 34619100 } 15 | , { offset = 120, start = 34306620 } 16 | , { offset = 60, start = 34094940 } 17 | , { offset = 120, start = 33782460 } 18 | , { offset = 60, start = 33570780 } 19 | , { offset = 120, start = 33258300 } 20 | , { offset = 60, start = 33046620 } 21 | , { offset = 120, start = 32734140 } 22 | , { offset = 60, start = 32512380 } 23 | , { offset = 120, start = 32209980 } 24 | , { offset = 60, start = 31988220 } 25 | , { offset = 120, start = 31685820 } 26 | , { offset = 60, start = 31464060 } 27 | , { offset = 120, start = 31151580 } 28 | , { offset = 60, start = 30939900 } 29 | , { offset = 120, start = 30627420 } 30 | , { offset = 60, start = 30415740 } 31 | , { offset = 120, start = 30103260 } 32 | , { offset = 60, start = 29881500 } 33 | , { offset = 120, start = 29579100 } 34 | , { offset = 60, start = 29357340 } 35 | , { offset = 120, start = 29054940 } 36 | , { offset = 60, start = 28833180 } 37 | , { offset = 120, start = 28530780 } 38 | , { offset = 60, start = 28309020 } 39 | , { offset = 120, start = 27996540 } 40 | , { offset = 60, start = 27784860 } 41 | , { offset = 120, start = 27472380 } 42 | , { offset = 60, start = 27260700 } 43 | , { offset = 120, start = 26948220 } 44 | , { offset = 60, start = 26726460 } 45 | , { offset = 120, start = 26424060 } 46 | , { offset = 60, start = 26202300 } 47 | , { offset = 120, start = 25899900 } 48 | , { offset = 60, start = 25678140 } 49 | , { offset = 120, start = 25365660 } 50 | , { offset = 60, start = 25153980 } 51 | , { offset = 120, start = 24841500 } 52 | , { offset = 60, start = 24629820 } 53 | , { offset = 120, start = 24317340 } 54 | , { offset = 60, start = 24095580 } 55 | , { offset = 120, start = 23793180 } 56 | , { offset = 60, start = 23571420 } 57 | , { offset = 120, start = 23269020 } 58 | , { offset = 60, start = 23047260 } 59 | , { offset = 120, start = 22744860 } 60 | , { offset = 60, start = 22523100 } 61 | , { offset = 120, start = 22210620 } 62 | , { offset = 60, start = 21998940 } 63 | , { offset = 120, start = 21686460 } 64 | , { offset = 60, start = 21474780 } 65 | , { offset = 120, start = 21162300 } 66 | , { offset = 60, start = 20940540 } 67 | , { offset = 120, start = 20638140 } 68 | , { offset = 60, start = 20416380 } 69 | , { offset = 120, start = 20113980 } 70 | , { offset = 60, start = 19892220 } 71 | , { offset = 120, start = 19579740 } 72 | , { offset = 60, start = 19368060 } 73 | , { offset = 120, start = 19055580 } 74 | , { offset = 60, start = 18843900 } 75 | , { offset = 120, start = 18531420 } 76 | , { offset = 60, start = 18319740 } 77 | , { offset = 120, start = 18007260 } 78 | , { offset = 60, start = 17785500 } 79 | , { offset = 120, start = 17483100 } 80 | , { offset = 60, start = 17261340 } 81 | , { offset = 120, start = 16958940 } 82 | , { offset = 60, start = 16737180 } 83 | , { offset = 120, start = 16424700 } 84 | , { offset = 60, start = 16213020 } 85 | , { offset = 120, start = 15900540 } 86 | , { offset = 60, start = 15688860 } 87 | , { offset = 120, start = 15376380 } 88 | , { offset = 60, start = 15154620 } 89 | , { offset = 120, start = 14852220 } 90 | , { offset = 60, start = 14630460 } 91 | , { offset = 120, start = 14328060 } 92 | , { offset = 60, start = 14106300 } 93 | , { offset = 120, start = 13803900 } 94 | , { offset = 60, start = 13531740 } 95 | , { offset = 120, start = 13269660 } 96 | , { offset = 60, start = 13007580 } 97 | , { offset = 120, start = 12745500 } 98 | , { offset = 60, start = 12483420 } 99 | , { offset = 120, start = 12221340 } 100 | , { offset = 60, start = 11959260 } 101 | , { offset = 120, start = 11697180 } 102 | , { offset = 60, start = 11435100 } 103 | , { offset = 120, start = 11173020 } 104 | , { offset = 60, start = 10910940 } 105 | , { offset = 120, start = 10638780 } 106 | , { offset = 60, start = 10376700 } 107 | , { offset = 120, start = 10114620 } 108 | , { offset = 60, start = 9852540 } 109 | , { offset = 120, start = 9590460 } 110 | , { offset = 60, start = 9328380 } 111 | , { offset = 120, start = 9066300 } 112 | , { offset = 60, start = 8804220 } 113 | , { offset = 120, start = 8542140 } 114 | , { offset = 60, start = 8280060 } 115 | , { offset = 120, start = 8017980 } 116 | , { offset = 60, start = 7755900 } 117 | , { offset = 120, start = 7483740 } 118 | , { offset = 60, start = 7221660 } 119 | , { offset = 120, start = 6959580 } 120 | , { offset = 60, start = 6697500 } 121 | , { offset = 120, start = 6435420 } 122 | , { offset = 60, start = 6173340 } 123 | , { offset = 120, start = 5911260 } 124 | , { offset = 60, start = 5649180 } 125 | , { offset = 120, start = 5397180 } 126 | , { offset = 60, start = 5125020 } 127 | , { offset = 120, start = 4862940 } 128 | , { offset = 60, start = 4600860 } 129 | , { offset = 120, start = 4338780 } 130 | , { offset = 60, start = 4066620 } 131 | , { offset = 120, start = 3814620 } 132 | , { offset = 60, start = 3542340 } 133 | , { offset = 120, start = 3280320 } 134 | ] 135 | -------------------------------------------------------------------------------- /src/View/SideMenu.elm: -------------------------------------------------------------------------------- 1 | module View.SideMenu exposing (view) 2 | 3 | import Calendar.Msg exposing (TimeSpan(..)) 4 | import Config exposing (allGroups) 5 | import Cyberplanning.Types as PlanningTypes exposing (Group) 6 | import Html exposing (Html, a, button, div, i, input, label, span, text) 7 | import Html.Attributes exposing (attribute, checked, class, for, href, id, style, target, title, type_) 8 | import Html.Events exposing (onCheck, onClick) 9 | import Model exposing (Model) 10 | import Msg exposing (Msg(..)) 11 | import MultiSelect exposing ( Options, multiSelect) 12 | import Personnel.Personnel as Personnel 13 | 14 | 15 | view : Model -> Html Msg 16 | view model = 17 | div 18 | [ class "sidemenu--container" 19 | , style "display" 20 | (if model.menuOpened then 21 | "flex" 22 | 23 | else 24 | "none" 25 | ) 26 | ] 27 | [ div 28 | [ class "sidemenu--main" ] 29 | ([ viewMultiSelector model.planningState.selectedGroups 30 | , hack2g2Checkbox model.planningState.settings.showHack2g2 31 | , customCheckbox model.planningState.settings.showCustom 32 | , modeButton Week "Week" 33 | , modeButton AllWeek "All Week" 34 | , modeButton Day "Day" 35 | ] ++ 36 | if Config.enablePersonnelCal then 37 | [ Html.map SetPersonnelState (Personnel.view model.personnelState) ] 38 | else 39 | []) 40 | , div 41 | [ class "sidemenu--footer" ] 42 | [ div 43 | [ class "sidemenu--footer-content" ] 44 | [ div [ class "sidemenu--row" ] [ githubButton ] 45 | , div 46 | [ class "sidemenu--row" ] 47 | [ a [] [ i [ class "icon-secure" ] [] ] 48 | , span [] [ text "Secured by" ] 49 | , span [ title "CyberPlanning", style "margin-left" "3px" ] [ text "CP" ] 50 | ] 51 | ] 52 | ] 53 | ] 54 | 55 | 56 | myOption : Options PlanningTypes.InternalMsg 57 | myOption = 58 | { items = List.map (\x -> { value = String.fromInt x.id, text = x.name, enabled = True }) allGroups 59 | , onChange = PlanningTypes.SetGroups 60 | } 61 | 62 | 63 | viewMultiSelector : List Group -> Html Msg 64 | viewMultiSelector selectedGroups = 65 | div [ class "sidemenu--selector" ] 66 | [ label [ for "select-group" ] [ text "Groupes" ] 67 | , multiSelect 68 | myOption 69 | [ class "sidemenu--selector" 70 | , style "color" "white" 71 | , id "select-group" 72 | ] 73 | (List.map (.id >> String.fromInt) selectedGroups) 74 | ] 75 | |> Html.map SetPlanningState 76 | 77 | 78 | hack2g2Checkbox : Bool -> Html Msg 79 | hack2g2Checkbox isChecked = 80 | div [ class "md-checkbox" ] 81 | [ input [ id "check-hack2g2", type_ "checkbox", checked isChecked, onCheck (PlanningTypes.CheckEvents PlanningTypes.Hack2g2) ] [] 82 | , label [ for "check-hack2g2" ] [ text "Hack2g2" ] 83 | ] 84 | |> Html.map SetPlanningState 85 | 86 | 87 | customCheckbox : Bool -> Html Msg 88 | customCheckbox isChecked = 89 | div [ class "md-checkbox" ] 90 | [ input [ id "check-custom", type_ "checkbox", checked isChecked, onCheck (PlanningTypes.CheckEvents PlanningTypes.Custom) ] [] 91 | , label [ for "check-custom" ] [ text "Custom" ] 92 | ] 93 | |> Html.map SetPlanningState 94 | 95 | 96 | modeButton : TimeSpan -> String -> Html Msg 97 | modeButton mode name = 98 | div [] 99 | [ button [ onClick (ChangeMode mode), attribute "aria-label" ("Toggle Mode " ++ name) ] [ text name ] 100 | ] 101 | 102 | 103 | githubButton : Html msg 104 | githubButton = 105 | a 106 | [ class "sidemenu--github-btn", href "https://github.com/cyberplanning/webclient", target "_blank" ] 107 | [ i [ class "icon-github" ] [] 108 | , span [] [ text "Star" ] 109 | ] 110 | -------------------------------------------------------------------------------- /src/View/Tooltip.elm: -------------------------------------------------------------------------------- 1 | module View.Tooltip exposing (viewTooltip) 2 | 3 | import Calendar.Event as CalEvent 4 | import Calendar.Msg exposing (Position) 5 | import Html exposing (Html, div, text, span) 6 | import Html.Attributes exposing (class, style) 7 | import Model exposing (WindowSize) 8 | import Msg exposing (Msg(..)) 9 | import MyTime 10 | import Time exposing (Posix) 11 | 12 | 13 | viewTooltip : Maybe CalEvent.Event -> Maybe Position -> WindowSize -> Html Msg 14 | viewTooltip maybeEvent maybePos screenSize = 15 | viewTooltipContent maybePos screenSize maybeEvent 16 | |> div [ class "tooltip" ] 17 | 18 | 19 | viewTooltipContent : Maybe Position -> WindowSize -> Maybe CalEvent.Event -> List (Html Msg) 20 | viewTooltipContent maybePos screenSize maybeEvent = 21 | case maybeEvent of 22 | Just event -> 23 | let 24 | badge = 25 | if String.isEmpty event.source then 26 | [] 27 | 28 | else 29 | [ viewBadge event.source event.style ] 30 | 31 | title = 32 | text event.title :: badge 33 | in 34 | [ div (class "tooltip--event" :: tooltipStylePos event.style maybePos screenSize) 35 | ([ div [ class "tooltip--event-title" ] title 36 | , div [ class "tooltip--event-sub", class "tooltip--event-hours" ] [ viewHour event ] 37 | ] 38 | ++ showIfNotEmpty event.description 39 | ) 40 | ] 41 | 42 | _ -> 43 | [] 44 | 45 | 46 | showIfNotEmpty : List String -> List (Html Msg) 47 | showIfNotEmpty data = 48 | List.filter (\e -> String.isEmpty e == False) data 49 | |> List.map (\e -> div [ class "tooltip--event-sub" ] [ text e ]) 50 | 51 | 52 | tooltipStylePos : CalEvent.Style -> Maybe Position -> WindowSize -> List (Html.Attribute Msg) 53 | tooltipStylePos { textColor, eventColor } maybePos { width, height } = 54 | let 55 | absoluteCoords = 56 | case maybePos of 57 | Just pos -> 58 | let 59 | posX = 60 | pos.x 61 | - 125 62 | |> Basics.min (width - 252) 63 | |> Basics.max 0 64 | |> String.fromInt 65 | 66 | posY = 67 | pos.y 68 | |> Basics.min (height - 152) 69 | |> Basics.max 0 70 | |> String.fromInt 71 | in 72 | [ style "left" (posX ++ "px"), style "top" (posY ++ "px") ] 73 | 74 | _ -> 75 | [ style "bottom" "0" ] 76 | in 77 | [ style "background-color" eventColor, style "color" textColor ] 78 | ++ absoluteCoords 79 | 80 | 81 | viewHour : CalEvent.Event -> Html Msg 82 | viewHour event = 83 | toString event.startTime 84 | ++ " - " 85 | ++ toString event.endTime 86 | |> text 87 | 88 | 89 | viewBadge : String -> CalEvent.Style -> Html Msg 90 | viewBadge name { eventColor, textColor } = 91 | span [ class "tooltip--event-badge", style "background-color" textColor, style "color" eventColor ] 92 | [ text name ] 93 | 94 | 95 | toString : Posix -> String 96 | toString time = 97 | let 98 | minutes = 99 | MyTime.toMinute time 100 | |> String.fromInt 101 | |> String.padLeft 2 '0' 102 | 103 | hours = 104 | MyTime.toHour time 105 | |> String.fromInt 106 | in 107 | hours ++ ":" ++ minutes 108 | -------------------------------------------------------------------------------- /src/View/View.elm: -------------------------------------------------------------------------------- 1 | module View.View exposing (view) 2 | 3 | import Browser exposing (Document) 4 | import Calendar.Calendar as Calendar 5 | import Calendar.Msg exposing (TimeSpan(..)) 6 | import Config 7 | import Cyberplanning.Types exposing (FetchStatus(..)) 8 | import Html exposing (Html, Attribute, div, h2, text, i, button, span) 9 | import Html.Attributes exposing (class, attribute, classList, style) 10 | import Html.Events exposing (onClick) 11 | import Http 12 | import Model exposing (Model) 13 | import Msg exposing (Msg(..)) 14 | import MyTime 15 | import Personnel.Personnel as Personnel 16 | import Secret.Help 17 | import Secret.Secret 18 | import Time exposing (Month(..), Posix) 19 | import Vendor.Swipe exposing (onSwipe) 20 | import View.SideMenu as SideMenu 21 | import View.Tooltip as Tooltip 22 | 23 | 24 | 25 | ---- VIEW ---- 26 | 27 | 28 | view : Model -> Document Msg 29 | view model = 30 | let 31 | events = 32 | if Secret.Secret.isHelpActivated model.secret then 33 | Secret.Help.helpEvents model.calendarState.viewing 34 | 35 | else 36 | model.planningState.events ++ Personnel.getEvents model.personnelState 37 | 38 | attrs = 39 | class "main--container" 40 | :: onSwipe SwipeEvent 41 | ++ Secret.Secret.classStyle model.secret 42 | 43 | currentEventId = 44 | if model.size.width < Config.minWeekWidth then 45 | model.calendarState.selected 46 | 47 | else 48 | model.calendarState.hover 49 | 50 | currentEvent = 51 | case currentEventId of 52 | Just id -> 53 | List.filter (\e -> e.toId == id) events 54 | |> List.head 55 | 56 | _ -> 57 | Nothing 58 | 59 | funThings = 60 | if Config.enableEasterEgg then 61 | Secret.Secret.view model.secret 62 | else 63 | Html.text "" 64 | 65 | container = 66 | div attrs 67 | [ viewToolbar model.calendarState.viewing (model.calendarState.timeSpan /= Day) model.loop model.planningState.status 68 | , div [ class "main--calendar" ] 69 | [ SideMenu.view model 70 | , Html.map SetCalendarState (Calendar.view events model.planningState.groupsCount model.calendarState) 71 | ] 72 | , Tooltip.viewTooltip currentEvent model.calendarState.position model.size 73 | , funThings 74 | ] 75 | 76 | names = 77 | List.map .name model.planningState.selectedGroups 78 | |> String.join ", " 79 | in 80 | { title = "Planning - " ++ names 81 | , body = [ container ] 82 | } 83 | 84 | 85 | viewToolbar : Posix -> Bool -> Bool -> FetchStatus -> Html Msg 86 | viewToolbar viewing displayArrows loop fetchStatus = 87 | let 88 | navigations = 89 | if displayArrows then 90 | [ viewArrowButton "icon-left" "Previous Page" (SetCalendarState Calendar.Msg.PageBack) 91 | , viewArrowButton "icon-right" "Next Page" (SetCalendarState Calendar.Msg.PageForward) 92 | ] 93 | 94 | else 95 | [] 96 | in 97 | div [ class "main--toolbar" ] 98 | (viewMenuButton 99 | :: navigations 100 | ++ [ viewTodayButton 101 | , viewTodayIconButton 102 | , viewTitle viewing 103 | , viewMessage fetchStatus 104 | , viewReloadButton loop 105 | ] 106 | ) 107 | 108 | 109 | viewTitle : Posix -> Html Msg 110 | viewTitle viewing = 111 | h2 [ class "main--month-title" ] 112 | [ text <| formatDateTitle viewing ] 113 | 114 | 115 | formatDateTitle : Posix -> String 116 | formatDateTitle date = 117 | let 118 | monthName = 119 | MyTime.toMonth date 120 | |> MyTime.monthToString 121 | 122 | year = 123 | MyTime.toYear date 124 | |> String.fromInt 125 | in 126 | monthName ++ " " ++ year 127 | 128 | 129 | viewArrowButton : String -> String -> Msg -> Html Msg 130 | viewArrowButton classname label msg = 131 | navButton 132 | [ class "main--navigatiors-button", onClick msg, attribute "aria-label" label ] 133 | [ i [ class classname ] [] ] 134 | 135 | 136 | viewTodayButton : Html Msg 137 | viewTodayButton = 138 | navButton [ class "main--navigatiors-today", onClick ClickToday, attribute "aria-label" "Today" ] [ text "aujourd'hui" ] 139 | 140 | 141 | viewTodayIconButton : Html Msg 142 | viewTodayIconButton = 143 | navButton 144 | [ class "main--navigatiors-todayicon", onClick ClickToday, attribute "aria-label" "Today" ] 145 | [ i [ class "icon-today" ] [] ] 146 | 147 | 148 | viewReloadButton : Bool -> Html Msg 149 | viewReloadButton loop = 150 | navButton 151 | [ classList 152 | [ ( "main--navigatiors-button", True ) 153 | , ( "main--navigatiors-reload", True ) 154 | , ( "loop", loop ) 155 | ] 156 | , onClick Reload 157 | , attribute "aria-label" "Reload" 158 | ] 159 | [ i [ class "icon-reload" ] [] 160 | ] 161 | 162 | 163 | viewMenuButton : Html Msg 164 | viewMenuButton = 165 | navButton 166 | [ class "main--navigatiors-button", onClick ToggleMenu, attribute "aria-label" "Toggle Menu" ] 167 | [ i [ class "icon-menu" ] [] 168 | ] 169 | 170 | 171 | navButton : List (Attribute msg) -> List (Html msg) -> Html msg 172 | navButton attr content = 173 | -- div 174 | -- [ class "main--navigatiors-action" ] 175 | -- [ button attr content ] 176 | button attr content 177 | 178 | 179 | viewMessage : FetchStatus -> Html Msg 180 | viewMessage fetchStatus = 181 | let 182 | content = 183 | case fetchStatus of 184 | Loading -> 185 | [ span [] [ text "Loading" ] ] 186 | 187 | Error err -> 188 | [ i [ class "icon-wifi", style "color" "#f9f961" ] [], span [ style "color" "#f9f961" ] [ errorMessage err |> text ] ] 189 | 190 | Normal -> 191 | [ span [] [ text "" ] ] 192 | in 193 | div [ class "main--status" ] content 194 | 195 | 196 | errorMessage : Http.Error -> String 197 | errorMessage error = 198 | case error of 199 | Http.BadUrl _ -> 200 | "BadUrl" 201 | 202 | Http.Timeout -> 203 | "Timeout" 204 | 205 | Http.NetworkError -> 206 | "Network Error" 207 | 208 | Http.BadStatus _ -> 209 | "BadStatus" 210 | 211 | Http.BadPayload _ _ -> 212 | "BadPayload" 213 | -------------------------------------------------------------------------------- /src/css/calendar.css: -------------------------------------------------------------------------------- 1 | .calendar--calendar { 2 | display: flex; 3 | flex: 1; 4 | } 5 | 6 | .calendar--time-gutter { 7 | display: flex; 8 | flex-direction: column; 9 | background-color: var(--background-color); 10 | } 11 | 12 | .calendar--time-slot-group { 13 | min-height: 30px; 14 | display: flex; 15 | flex-flow: column nowrap; 16 | flex: 1; 17 | border-bottom: 1px solid var(--border-color); 18 | border-left: 1px solid var(--border-color); 19 | } 20 | 21 | .calendar--time-slot-text { 22 | font-size: 12px; 23 | } 24 | 25 | .calendar--week { 26 | flex: 1; 27 | overflow-y: auto; 28 | } 29 | 30 | .calendar--week-content { 31 | display: flex; 32 | overflow: hidden; 33 | /* flex: 1; */ 34 | min-height: 100%; 35 | } 36 | 37 | .calendar--dates { 38 | flex-flow: column nowrap; 39 | display: flex; 40 | align-content: stretch; 41 | justify-content: space-around; 42 | flex: 1; 43 | min-width: 0; 44 | } 45 | 46 | .calendar--date-header { 47 | justify-content: center; 48 | display: flex; 49 | align-items: center; 50 | 51 | background-color: var(--accent-color); 52 | padding: 2px 0; 53 | border-bottom: 2px solid var(--header-border-color); 54 | } 55 | 56 | .calendar--date-header-weeknum { 57 | background-color: var(--accent-color-dark); 58 | } 59 | 60 | .calendar--date-header-zone { 61 | font-size: 9px; 62 | padding: 1px 0; 63 | border-bottom: 1px solid var(--border-color); 64 | } 65 | 66 | .calendar--date-header-content { 67 | flex: 1; 68 | display: flex; 69 | align-items: center; 70 | justify-content: center; 71 | } 72 | 73 | .calendar--date { 74 | text-decoration: none; 75 | font-size: 15px; 76 | line-height: 26px; 77 | white-space: nowrap; 78 | overflow: hidden; 79 | text-overflow: ellipsis; 80 | } 81 | 82 | .calendar--day { 83 | display: flex; 84 | flex-flow: column nowrap; 85 | flex: 1; 86 | } 87 | 88 | .calendar--day-slot { 89 | flex: 1; 90 | position: relative; 91 | display: flex; 92 | flex-direction: column; 93 | overflow: hidden; 94 | } 95 | 96 | .calendar--day-content { 97 | display: flex; 98 | flex: 1; 99 | overflow: auto; 100 | } 101 | 102 | .calendar--hour-slot { 103 | padding: 0 10px; 104 | flex: 1 0 0; 105 | } 106 | 107 | .calendar--event { 108 | font-size: 0.9rem; 109 | padding: 0.2em; 110 | background-color: var(--accent-color); 111 | color: white; 112 | cursor: pointer; 113 | overflow: hidden; 114 | border-bottom: 1px solid var(--border-color); 115 | user-select: none; 116 | } 117 | 118 | .calendar--event-title { 119 | font-weight: 500; 120 | font-size: 0.9rem; 121 | max-height: 2.4em; 122 | overflow: hidden; 123 | } 124 | 125 | .calendar--event-sub { 126 | font-size: 11px; 127 | } 128 | 129 | .calendar--navigations-day, 130 | .calendar--navigations-week { 131 | border: 0; 132 | padding: 0 15px; 133 | background-color: transparent; 134 | height: 100%; 135 | font-size: 15px; 136 | cursor: pointer; 137 | } 138 | 139 | /* .calendar--navigations-day:hover, 140 | .calendar--navigations-week:hover { 141 | font-weight: bold; 142 | } */ 143 | 144 | .calendar--jour-ferie { 145 | position: absolute; 146 | top: 0; 147 | height: 100%; 148 | background-color: #262d2f; 149 | width: 100%; 150 | color: #657277; 151 | padding: 100px 0; 152 | border-left: 1px solid var(--border-color); 153 | } 154 | -------------------------------------------------------------------------------- /src/css/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --nav-color: #124368; 3 | --accent-color: #3c6382; 4 | --accent-color-dark: #345b7a; 5 | 6 | --background-color: #262d2f; 7 | --body-color: #2d3436; 8 | --border-color: #363e41; 9 | --text-color: #ecf0f1; 10 | 11 | --header-border-color: #122e42; 12 | } 13 | -------------------------------------------------------------------------------- /src/css/cybericons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'cybericons'; 3 | src: url('font/cybericons.eot?33213932'); 4 | src: url('font/cybericons.eot?33213932#iefix') format('embedded-opentype'), url('font/cybericons.woff2?33213932') format('woff2'), url('font/cybericons.woff?33213932') format('woff'), 5 | url('font/cybericons.ttf?33213932') format('truetype'), url('font/cybericons.svg?33213932#cybericons') format('svg'); 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 10 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 11 | /* 12 | @media screen and (-webkit-min-device-pixel-ratio:0) { 13 | @font-face { 14 | font-family: 'cybericons'; 15 | src: url('font/cybericons.svg?33213932#cybericons') format('svg'); 16 | } 17 | } 18 | */ 19 | 20 | [class^='icon-']:before, 21 | [class*=' icon-']:before { 22 | font-family: 'cybericons'; 23 | font-style: normal; 24 | font-weight: normal; 25 | speak: none; 26 | 27 | display: inline-block; 28 | text-decoration: inherit; 29 | width: 1em; 30 | margin-right: 0.2em; 31 | text-align: center; 32 | /* opacity: .8; */ 33 | 34 | /* For safety - reset parent styles, that can break glyph codes*/ 35 | font-variant: normal; 36 | text-transform: none; 37 | 38 | /* fix buttons height, for twitter bootstrap */ 39 | line-height: 1em; 40 | 41 | /* Animation center compensation - margins should be symmetric */ 42 | /* remove if not needed */ 43 | margin-left: 0.2em; 44 | 45 | /* you can be more comfortable with increased icons size */ 46 | /* font-size: 120%; */ 47 | 48 | /* Font smoothing. That was taken from TWBS */ 49 | -webkit-font-smoothing: antialiased; 50 | -moz-osx-font-smoothing: grayscale; 51 | 52 | /* Uncomment for 3D effect */ 53 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 54 | } 55 | 56 | .icon-left:before { 57 | content: '\e800'; 58 | transform: translateX(-2px); 59 | } /* '' */ 60 | .icon-right:before { 61 | content: '\e801'; 62 | } /* '' */ 63 | .icon-unsecure:before { 64 | content: '\e802'; 65 | } /* '' */ 66 | .icon-secure:before { 67 | content: '\e803'; 68 | } /* '' */ 69 | .icon-warn:before { 70 | content: '\e804'; 71 | } /* '' */ 72 | .icon-reload:before { 73 | content: '\e832'; 74 | } /* '' */ 75 | .icon-github:before { 76 | content: '\f09b'; 77 | } /* '' */ 78 | .icon-menu:before { 79 | content: '\f0c9'; 80 | } /* '' */ 81 | .icon-today:before { 82 | content: '\f133'; 83 | } /* '' */ 84 | .icon-wifi:before { 85 | content: '\f1eb'; 86 | } /* '' */ 87 | -------------------------------------------------------------------------------- /src/css/font/cybericons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/src/css/font/cybericons.eot -------------------------------------------------------------------------------- /src/css/font/cybericons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20190801 at Fri Nov 8 09:39:08 2019 6 | By Hedroed 7 | Copyright (C) 2019 by original authors @ fontello.com 8 | 9 | 10 | 11 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 39 | 43 | 48 | 51 | 54 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/css/font/cybericons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/src/css/font/cybericons.ttf -------------------------------------------------------------------------------- /src/css/font/cybericons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/src/css/font/cybericons.woff -------------------------------------------------------------------------------- /src/css/font/cybericons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberPlanning/webclient/e37e6c0fcc2ab54c0af27ae1589bebd9a97bdac7/src/css/font/cybericons.woff2 -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,500'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 16px; 9 | } 10 | 11 | html, 12 | body { 13 | padding: 0; 14 | margin: 0; 15 | height: 100vh; 16 | overflow: hidden; 17 | } 18 | 19 | body { 20 | margin: 0; 21 | text-align: center; 22 | background-color: var(--body-color); 23 | } 24 | 25 | body, 26 | button, 27 | a, 28 | input, 29 | select { 30 | font-family: 'Roboto', Helvetica, Arial, sans-serif; 31 | color: var(--text-color); 32 | } 33 | 34 | button { 35 | border: 0; 36 | border-radius: 0; 37 | background-color: transparent; 38 | cursor: pointer; 39 | padding: 5px 15px; 40 | } 41 | 42 | button:hover { 43 | background-color: var(--accent-color); 44 | } 45 | 46 | .hidden { 47 | display: none !important; 48 | } 49 | 50 | .main--container { 51 | height: 100%; 52 | display: flex; 53 | flex-direction: column; 54 | flex: 1; 55 | } 56 | 57 | .main--calendar { 58 | overflow: hidden; 59 | display: flex; 60 | flex: 1; 61 | position: relative; 62 | } 63 | 64 | .main--toolbar { 65 | display: flex; 66 | background-color: var(--nav-color); 67 | padding: 0 0.5rem; 68 | justify-content: space-between; 69 | height: 2.5rem; 70 | position: relative; 71 | overflow: hidden; 72 | /* flex-wrap: wrap; */ 73 | } 74 | 75 | .main--toolbar > * { 76 | margin: 0.3125rem 0.5rem; 77 | } 78 | 79 | .main--month-title { 80 | display: flex; 81 | flex: 1; 82 | padding-left: 0.4em; 83 | align-items: center; 84 | font-size: 1.3rem; 85 | white-space: nowrap; 86 | } 87 | 88 | .main--navigatiors-button { 89 | border: 0; 90 | background-color: transparent; 91 | font-size: 1rem; 92 | cursor: pointer; 93 | vertical-align: top; 94 | } 95 | 96 | .main--navigatiors-button:hover { 97 | background-color: var(--accent-color); 98 | } 99 | 100 | button:focus, 101 | select:focus, 102 | option:focus, 103 | input:focus { 104 | outline-color: var(--accent-color); 105 | } 106 | 107 | .main--navigatiors-reload i { 108 | display: block; 109 | transform-origin: center center; 110 | animation-name: none; 111 | animation-duration: 1000ms; 112 | animation-timing-function: ease-in-out; 113 | animation-iteration-count: 1; 114 | } 115 | 116 | .main--navigatiors-reload.loop i { 117 | animation-name: loop; 118 | } 119 | 120 | .main--navigatiors-action { 121 | display: inline-flex; 122 | vertical-align: top; 123 | padding: 5px 0 5px 5px; 124 | height: 100%; 125 | } 126 | 127 | .main--navigatiors-button { 128 | padding: 0 0.2375rem; 129 | border-radius: 50%; 130 | } 131 | 132 | .main--navigatiors-today { 133 | padding: 0 8px; 134 | text-transform: uppercase; 135 | font-weight: 500; 136 | font-size: 0.9rem; 137 | background-color: var(--accent-color); 138 | border-radius: 5px; 139 | vertical-align: top; 140 | } 141 | 142 | .main--navigatiors-today:active, 143 | .main--navigatiors-todayicon:active { 144 | background-color: var(--accent-color-dark); 145 | } 146 | 147 | .main--navigatiors-todayicon { 148 | display: none; 149 | font-size: 1rem; 150 | padding: 0 0.25em; 151 | background-color: var(--accent-color); 152 | border-radius: 50%; 153 | } 154 | 155 | .main--status { 156 | display: flex; 157 | align-items: center; 158 | padding: 0 0.5rem; 159 | } 160 | 161 | .main--status i { 162 | animation: blur 0.75s ease-out infinite; 163 | } 164 | 165 | @keyframes blur { 166 | from { 167 | text-shadow: 0 0 0.1em, 0 0 0.25em, 0 0 0.5em, 0 0 1.5em, 0 0.1em 1em, 0 -0.1em 1em; 168 | } 169 | } 170 | 171 | @keyframes loop { 172 | from { 173 | transform: rotate(0); 174 | } 175 | 176 | to { 177 | transform: rotate(360deg); 178 | } 179 | } 180 | 181 | @media screen and (max-width: 600px) { 182 | .main--status span { 183 | display: none; 184 | } 185 | 186 | .main--toolbar > * { 187 | margin: 0.3125rem 0.1rem; 188 | } 189 | } 190 | 191 | @media only screen and (max-width: 500px) { 192 | .main--month-title { 193 | font-size: 1rem; 194 | } 195 | 196 | .main--navigatiors-today { 197 | display: none; 198 | } 199 | .main--navigatiors-todayicon { 200 | display: block; 201 | } 202 | 203 | .calendar--hour-slot { 204 | padding: 0 3px !important; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/css/material-checkbox.css: -------------------------------------------------------------------------------- 1 | .md-checkbox { 2 | position: relative; 3 | margin: 1em 0; 4 | text-align: left; 5 | } 6 | .md-checkbox.md-checkbox-inline { 7 | display: inline-block; 8 | } 9 | .md-checkbox label { 10 | cursor: pointer; 11 | display: inline; 12 | line-height: 1.25em; 13 | vertical-align: top; 14 | clear: both; 15 | padding-left: 1px; 16 | box-sizing: border-box; 17 | } 18 | .md-checkbox label:not(:empty) { 19 | padding-left: 0.75em; 20 | } 21 | .md-checkbox label:before, .md-checkbox label:after { 22 | content: ""; 23 | position: absolute; 24 | left: 0; 25 | top: 0; 26 | box-sizing: border-box; 27 | } 28 | .md-checkbox label:before { 29 | width: 1.25em; 30 | height: 1.25em; 31 | background: var(--text-color); 32 | border: 2px solid var(--border-color); 33 | border-radius: 0.125em; 34 | cursor: pointer; 35 | transition: background .3s; 36 | } 37 | .md-checkbox input[type="checkbox"] { 38 | width: 1.25em; 39 | height: 1.25em; 40 | margin: 0; 41 | display: block; 42 | float: left; 43 | font-size: inherit; 44 | outline-color: var(--text-color); 45 | } 46 | .md-checkbox input[type="checkbox"]:checked + label:before { 47 | background: var(--accent-color); 48 | border: none; 49 | } 50 | .md-checkbox input[type="checkbox"]:checked + label:after { 51 | transform: translate(0.25em, 0.3365384615em) rotate(-45deg); 52 | width: 0.75em; 53 | height: 0.375em; 54 | border: 0.125em solid var(--text-color); 55 | border-top-style: none; 56 | border-right-style: none; 57 | } 58 | .md-checkbox input[type="checkbox"]:disabled + label:before { 59 | border-color: rgba(0, 0, 0, 0.26); 60 | } 61 | .md-checkbox input[type="checkbox"]:disabled:checked + label:before { 62 | background: rgba(0, 0, 0, 0.26); 63 | } -------------------------------------------------------------------------------- /src/css/personnel.css: -------------------------------------------------------------------------------- 1 | .personnel--fileinfo { 2 | cursor: pointer; 3 | padding: 0.5rem 1rem; 4 | background-color: var(--accent-color); 5 | } 6 | 7 | .personnel--fileinfo-remove { 8 | font-size: 0.7em; 9 | width: 90%; 10 | display: inline-block; 11 | background-color: var(--nav-color); 12 | } 13 | -------------------------------------------------------------------------------- /src/css/secret.css: -------------------------------------------------------------------------------- 1 | .fun2 .calendar--event { 2 | background-color: #e30404 !important; 3 | overflow: visible; 4 | animation: 4s singing infinite alternate linear; 5 | } 6 | 7 | .fun2 .calendar--week { 8 | background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Communist_Party_of_Vietnam_flag_logo.svg/277px-Communist_Party_of_Vietnam_flag_logo.svg.png'); 9 | background-repeat: no-repeat; 10 | background-position: center center; 11 | background-size: 90vh; 12 | } 13 | 14 | .fun2 .calendar--week-content .calendar--dates:nth-child(odd) .calendar--event { 15 | animation-delay: 1s; 16 | } 17 | 18 | .fun2 .calendar--week-content .calendar--dates:nth-child(even) .calendar--event { 19 | animation-delay: 0; 20 | } 21 | 22 | .fun2 .calendar--event::after { 23 | content: ''; 24 | background-image: url("http://en.shapki-ushanki.ru/img/ushanka.png"); 25 | width: 100%; 26 | max-width: 150px; 27 | height: 100px; 28 | display: block; 29 | background-size: cover; 30 | position: absolute; 31 | top: -80px; 32 | left: 50%; 33 | transform: translateX(-50%); 34 | } 35 | 36 | .fun2 .calendar--event::before { 37 | content: ''; 38 | background-image: url("http://gifimage.net/wp-content/uploads/2017/08/mouth-gif-21.gif"); 39 | width: 50%; 40 | max-width: 50px; 41 | height: 50px; 42 | display: block; 43 | background-size: cover; 44 | position: absolute; 45 | bottom: 10%; 46 | left: 50%; 47 | transform: translateX(-50%); 48 | mix-blend-mode: multiply; 49 | } 50 | 51 | .fun .calendar--week-content .calendar--dates:nth-child(odd) .calendar--event:nth-child(odd) { 52 | animation: linear infinite 4s 0.6s fun alternate !important; 53 | } 54 | 55 | .fun .calendar--week-content .calendar--dates:nth-child(odd) .calendar--event:nth-child(even) { 56 | animation: linear infinite 4s 0.2s fun alternate !important; 57 | } 58 | 59 | .fun .calendar--week-content .calendar--dates:nth-child(even) .calendar--event:nth-child(odd) { 60 | animation: linear infinite 4s fun alternate-reverse !important; 61 | } 62 | 63 | .fun .calendar--week-content .calendar--dates:nth-child(even) .calendar--event:nth-child(even) { 64 | animation: linear infinite 4s 0.5s fun alternate-reverse !important; 65 | } 66 | 67 | .fun .main--navigatiors-reload i { 68 | animation: linear infinite 4s 0.3s fun alternate-reverse !important; 69 | } 70 | 71 | .fun-help .calendar--event { 72 | transition: 1s; 73 | } 74 | 75 | @keyframes fun { 76 | 0% { 77 | transform: rotate(0) scale(0.8); 78 | } 79 | 80 | 50% { 81 | transform: rotate(20deg) scale(1.2); 82 | } 83 | 84 | 100% { 85 | transform: rotate(-720deg) scale(0.8); 86 | } 87 | } 88 | 89 | @keyframes singing { 90 | 0% { 91 | transform: translatey(0) scale(1); 92 | } 93 | 94 | 80% { 95 | transform: translatey(-15px) scale(1.05) rotate(1deg); 96 | } 97 | 98 | 100% { 99 | transform: translatey(-20px) scale(1.1) rotate(-1deg); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/css/sidemenu.css: -------------------------------------------------------------------------------- 1 | .sidemenu--container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .sidemenu--main { 7 | padding: 15px; 8 | flex: 1; 9 | } 10 | 11 | .sidemenu--selector select { 12 | display: block; 13 | border: 0; 14 | padding: 2px; 15 | background-color: var(--accent-color); 16 | font-size: 1rem; 17 | cursor: pointer; 18 | vertical-align: top; 19 | } 20 | 21 | .sidemenu--footer { 22 | padding: 3px 0; 23 | } 24 | 25 | .sidemenu--footer .icon-secure { 26 | color: green; 27 | } 28 | 29 | .sidemenu--footer .icon-unsecure { 30 | color: rgb(180, 40, 40); 31 | } 32 | 33 | .sidemenu--row { 34 | margin-bottom: 5px; 35 | } 36 | 37 | .sidemenu--github-btn { 38 | background-color: #eff3f6; 39 | background-image: linear-gradient(180deg, #fafbfc, #eff3f6 90%); 40 | border: 1px solid rgba(27, 31, 35, 0.2); 41 | padding: 0 5px; 42 | font-size: 0.8rem; 43 | font-weight: 600; 44 | cursor: pointer; 45 | border-radius: 0.25em; 46 | color: #24292e; 47 | text-decoration: none; 48 | box-sizing: content-box; 49 | display: inline-flex; 50 | } 51 | 52 | .sidemenu--github-btn i { 53 | font-size: 1.1em; 54 | } 55 | 56 | .sidemenu--github-btn span { 57 | padding: 0.1em 0.3em; 58 | } 59 | 60 | .sidemenu--github-btn svg { 61 | display: inline-block; 62 | vertical-align: text-top; 63 | fill: currentColor; 64 | } 65 | -------------------------------------------------------------------------------- /src/css/tooltip.css: -------------------------------------------------------------------------------- 1 | 2 | .tooltip--event { 3 | position: absolute; 4 | font-size: 0.9rem; 5 | width: 250px; 6 | color: var(--text-color); 7 | box-shadow: 1px 1px 5px rgba(0,0,0,0.7); 8 | 9 | pointer-events: none; 10 | user-select: none; 11 | } 12 | 13 | .tooltip--event-title { 14 | font-size: 1.3em; 15 | padding: 5px; 16 | } 17 | 18 | .tooltip--event-badge { 19 | font-weight: 500; 20 | font-size: 0.7rem; 21 | padding: 1px; 22 | border-radius: 3px; 23 | margin-left: 6px; 24 | vertical-align: baseline; 25 | background-color: var(--background-color); 26 | } 27 | 28 | .tooltip--event-hours { 29 | font-size: 1.2em; 30 | font-style: italic; 31 | } 32 | 33 | .tooltip--event-sub { 34 | background-color: var(--body-color); 35 | border-top: 1px solid var(--border-color); 36 | color: var(--text-color); 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './css/colors.css' 2 | import './css/main.css' 3 | import './css/tooltip.css' 4 | import './css/calendar.css' 5 | import './css/personnel.css' 6 | import './css/sidemenu.css' 7 | import './css/secret.css' 8 | import './css/cybericons.css' 9 | import './css/material-checkbox.css' 10 | import { Elm } from './Main.elm' 11 | import registerServiceWorker from './service/registerServiceWorker' 12 | 13 | const cyberplanning = localStorage.getItem('cyberplanning') || '' 14 | const personnel = localStorage.getItem('personnel') || '' 15 | 16 | const app = Elm.Main.init({ 17 | node: document.getElementById('root'), 18 | flags: { 19 | graphqlUrl: 'https://cyberplanning.fr/graphql', 20 | cyberplanning, 21 | personnel, 22 | }, 23 | }) 24 | 25 | if (app.ports) { 26 | app.ports.saveState.subscribe(function(data) { 27 | // console.log('Save state', data[0]) 28 | localStorage.setItem(data[0], data[1]) 29 | }) 30 | } 31 | global.app = app 32 | 33 | registerServiceWorker() 34 | 35 | var _0x6ba2 = [ 36 | '\x66\x6C\x61\x67', 37 | '\x67\x72\x6F\x75\x70\x43\x6F\x6C\x6C\x61\x70\x73\x65\x64', 38 | '\x54\x68\x33\x5F\x42\x34\x73\x31\x63\x5F\x4C\x30\x67\x5F\x30\x66\x5F\x44\x33\x61\x37\x68', 39 | '\x69\x6E\x66\x6F', 40 | '\x67\x72\x6F\x75\x70\x45\x6E\x64', 41 | ] 42 | console[_0x6ba2[1]](_0x6ba2[0]) 43 | console[_0x6ba2[3]](_0x6ba2[2]) 44 | console[_0x6ba2[4]](_0x6ba2[0]) 45 | -------------------------------------------------------------------------------- /src/service/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | // The URL constructor is available in all browsers that support SW. 14 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location) 15 | if (publicUrl.origin !== window.location.origin) { 16 | // Our service worker won't work if PUBLIC_URL is on a different origin 17 | // from what our page is served on. This might happen if a CDN is used to 18 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 19 | return 20 | } 21 | 22 | window.addEventListener('load', () => { 23 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 24 | checkValidServiceWorker(swUrl) 25 | }) 26 | } 27 | } 28 | 29 | function registerValidSW(swUrl) { 30 | navigator.serviceWorker 31 | .register(swUrl) 32 | .then(registration => { 33 | console.log('Service worker registered.') 34 | registration.onupdatefound = () => { 35 | const installingWorker = registration.installing 36 | installingWorker.onstatechange = () => { 37 | if (installingWorker.state === 'installed') { 38 | if (navigator.serviceWorker.controller) { 39 | // At this point, the old content will have been purged and 40 | // the fresh content will have been added to the cache. 41 | // It's the perfect time to display a "New content is 42 | // available; please refresh." message in your web app. 43 | console.log('New content is available; please refresh.') 44 | } else { 45 | // At this point, everything has been precached. 46 | // It's the perfect time to display a 47 | // "Content is cached for offline use." message. 48 | console.log('Content is cached for offline use.') 49 | } 50 | } 51 | } 52 | } 53 | }) 54 | .catch(error => { 55 | console.error('Error during service worker registration:', error) 56 | }) 57 | } 58 | 59 | function checkValidServiceWorker(swUrl) { 60 | // Check if the service worker can be found. If it can't reload the page. 61 | fetch(swUrl) 62 | .then(response => { 63 | // Ensure service worker exists, and that we really are getting a JS file. 64 | if (response.status === 404 || response.headers.get('content-type').indexOf('javascript') === -1) { 65 | console.log('Service worker not found (Unregister)') 66 | // No service worker found. Probably a different app. Reload the page. 67 | navigator.serviceWorker.ready.then(registration => { 68 | registration.unregister().then(() => { 69 | window.location.reload() 70 | }) 71 | }) 72 | } else { 73 | // Service worker found. Proceed as normal. 74 | registerValidSW(swUrl) 75 | } 76 | }) 77 | .catch(() => { 78 | console.log('No internet connection found. App is running in offline mode.') 79 | }) 80 | } 81 | 82 | export function unregister() { 83 | if ('serviceWorker' in navigator) { 84 | navigator.serviceWorker.ready.then(registration => { 85 | registration.unregister() 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (all) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | 6 | 7 | 8 | -- Check out http://package.elm-lang.org/packages/elm-community/elm-test/latest to learn more about testing in Elm! 9 | 10 | 11 | all : Test 12 | all = 13 | describe "A Test Suite" 14 | [ test "Addition" <| 15 | \_ -> 16 | Expect.equal 10 (3 + 7) 17 | , test "String.left" <| 18 | \_ -> 19 | Expect.equal "a" (String.left 1 "abcdefg") 20 | -- , test "This test should fail" <| 21 | -- \_ -> 22 | -- Expect.fail "failed as expected!" 23 | ] 24 | -------------------------------------------------------------------------------- /tests/Timeout.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (Model, Msg(..), main, model, myTask, update, view) 2 | 3 | import Html as App exposing (..) 4 | import Html.Events exposing (..) 5 | import Process 6 | import Task 7 | 8 | 9 | type alias Model = 10 | String 11 | 12 | 13 | model : Model 14 | model = 15 | "not clicked" 16 | 17 | 18 | myTask : Cmd Msg 19 | myTask = 20 | Process.sleep (2 * 1000) 21 | |> Task.andThen (\_ -> Task.succeed (Debug.log "timeout" "not clicked")) 22 | |> Task.perform Foo 23 | 24 | 25 | type Msg 26 | = Foo String 27 | | Bar 28 | | NoOp () 29 | 30 | 31 | update : Msg -> Model -> ( Model, Cmd Msg ) 32 | update msg model = 33 | case msg of 34 | Foo str -> 35 | ( str, Cmd.none ) 36 | 37 | Bar -> 38 | let 39 | txt = 40 | if model == "not clicked" then 41 | "clicked" 42 | 43 | else 44 | "not clickeed" 45 | 46 | cmd = 47 | if model == "not clicked" then 48 | myTask 49 | 50 | else 51 | Cmd.none 52 | in 53 | ( txt, cmd ) 54 | 55 | NoOp _ -> 56 | ( model, Cmd.none ) 57 | 58 | 59 | view : Model -> Html Msg 60 | view model = 61 | div [] 62 | [ button [ onClick Bar ] [ text "Click" ] 63 | , text model 64 | ] 65 | 66 | 67 | main : Program Never Model Msg 68 | main = 69 | App.program 70 | { init = ( model, myTask ) 71 | , view = view 72 | , update = update 73 | , subscriptions = \_ -> Sub.none 74 | } 75 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Test Suites", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0", 14 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 15 | }, 16 | "elm-version": "0.18.0 <= v < 0.19.0" 17 | } 18 | --------------------------------------------------------------------------------