├── .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 You need to enable JavaScript to run this app.
--------------------------------------------------------------------------------
/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 |
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 |
19 | You need to enable JavaScript to run this app.
20 |
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 |
146 |
147 |
148 |
149 |
150 |
160 |
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 |
--------------------------------------------------------------------------------