├── .gitignore
├── .prettierrc
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── LICENSE
├── README.md
├── assets
├── milestones.json
└── templates
│ ├── addLog.template.html
│ ├── dashboard.template.html
│ ├── logs.template.html
│ └── milestones.template.html
├── package.json
├── resources
├── 100-days-of-code-icon.png
├── dark
│ ├── dashboard.svg
│ ├── learn-more.svg
│ ├── logs.svg
│ ├── milestones.svg
│ ├── refresh copy.svg
│ ├── refresh-1.svg
│ └── refresh.svg
└── light
│ ├── dashboard.svg
│ ├── learn-more.svg
│ ├── logs.svg
│ ├── milestones.svg
│ ├── refresh-1.svg
│ ├── refresh.png
│ └── refresh.svg
├── src
├── extension.ts
├── managers
│ ├── FileManager.ts
│ └── MilestoneEventManager.ts
├── models
│ ├── CodetimeMetrics.ts
│ ├── Log.ts
│ ├── Milestone.ts
│ ├── Summary.ts
│ └── TreeNode.ts
├── test
│ ├── runTest.ts
│ └── suite
│ │ ├── extension.test.ts
│ │ └── index.ts
├── tree
│ ├── DoCTreeItem.ts
│ ├── Tree100DoCProvider.ts
│ └── TreeButtonManager.ts
└── utils
│ ├── AddLogUtil.ts
│ ├── CommandUtil.ts
│ ├── Constants.ts
│ ├── DashboardUtil.ts
│ ├── LanguageUtil.ts
│ ├── LogsTemplateUtil.ts
│ ├── LogsUtil.ts
│ ├── MetricUtil.ts
│ ├── MilestonesTemplateUtil.ts
│ ├── MilestonesUtil.ts
│ ├── PluginUtil.ts
│ ├── SummaryUtil.ts
│ └── Util.ts
├── swdc-100-days-of-code-1.2.1.vsix
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | out
3 | .DS_Store
4 | dist/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "trailingComma": "es5",
4 | "printWidth": 150
5 | }
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "runtimeExecutable": "${execPath}",
13 | "args": [
14 | "--extensionDevelopmentPath=${workspaceFolder}"
15 | ],
16 | "outFiles": [
17 | "${workspaceFolder}/out/**/*.js"
18 | ],
19 | "preLaunchTask": "${defaultBuildTask}"
20 | },
21 | {
22 | "name": "Extension Tests",
23 | "type": "extensionHost",
24 | "request": "launch",
25 | "runtimeExecutable": "${execPath}",
26 | "args": [
27 | "--extensionDevelopmentPath=${workspaceFolder}",
28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
29 | ],
30 | "outFiles": [
31 | "${workspaceFolder}/out/test/**/*.js"
32 | ],
33 | "preLaunchTask": "${defaultBuildTask}"
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off",
11 | "editor.tabSize": 2,
12 | "editor.formatOnSave": true,
13 | "editor.insertSpaces": false
14 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | out/test/**
4 | src/**
5 | .gitignore
6 | **/tsconfig.json
7 | **/.eslintrc.json
8 | **/*.map
9 | **/*.ts
10 |
--------------------------------------------------------------------------------
/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 2018 Software
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > :warning: **This plugin is no longer being maintained.** You can continue to track your progress towards completing the challenge using Code Time .
2 |
3 |
4 |
5 |
6 |  
7 | 100 Days of Code
8 |
9 |  
10 |
11 |
12 |
13 |
14 |
15 | 100 Days of Code is a VS Code extension that helps you reach your goals and complete the #100DaysOfCode Challenge.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ## The 100 Days Of Code Challenge
35 |
36 | The 100 Days of Code is a coding challenge created by [Alexander Kallaway](https://twitter.com/ka11away) to encourage people to learn new coding skills. The challenge follows one simple rule:
37 |
38 | - **Code for at least an hour each day for 100 consecutive days.**
39 |
40 | Tens of thousands of developers from around the world have taken on this challenge. Whether you are learning to code or an experienced developer, everyone—of any skill level—can participate in the 100 Days of Code challenge.
41 |
42 | We want to do our part to make your experience a little easier (and more fun too!). To get a complete overview of the challenge and why you should join the community, visit the [100 Days of Code official website](https://www.100daysofcode.com/), or check out our [Essential Guide to the 100 Days of Code](https://www.software.com/src/essential-guide-to-the-100-days-of-code-challenge).
43 |
44 |
45 |
46 | ## Features
47 |
48 | - **Track your progress**: Log daily progress, metrics, and milestones to see how far you have progressed over the 100 days.
49 |
50 | - **Collect milestones**: Work toward milestones while coding and unlock badges for your accomplishments along the way.
51 |
52 | - **Build your dashboard**: Quickly view your overall milestone progress, recent logs, and overall code time throughout the challenge.
53 |
54 | - **Share your progress**: Easily share your logs and milestones to Twitter.
55 |
56 | - **Backup your data**: Create a free Software account and we’ll back up your logs and milestones so you can pick up where you left off—even if you take a break or switch computers.
57 |
58 | The 100 Days of Code plugin is built on [Code Time](https://www.software.com/code-time), our powerful time tracking extension backed by a community of over 70,000 developers.
59 |
60 |
61 |
62 | ## Getting started
63 |
64 | ### **1. Create your web account**
65 |
66 | 100 Days of Code requires that you're logged in via Google, Github or your Software account. This allows your data to be accessible and synced across multiple devices.
67 |
68 |
69 |
70 |
71 |
72 | ### **2. Create a log entry**
73 |
74 | Logs are journal entries for your 100 Days of Code challenge to help you stay on track and remember all of your accomplishments. Each log entry includes a title, description, and set of links that you can add if you want to link to your projects and resources. Logs are also automatically populated with your code time, number of keystrokes, and lines added for that day of coding.
75 |
76 |
77 |
78 |
79 |
80 | Open the Code Time view in the sidebar and navigate to 100 Days of Code section. Open the **Add Log** page by clicking **View Logs**. Click the **Add Log** link at the top of the new window.
81 |
82 | You can only add one log entry per day. Any coding metrics (like code time) recorded after you submit your log entry will be automatically updated in the log. The title, description, and links for any log entry can be edited at any time.
83 |
84 |
85 |
86 |
87 |
88 | Logging your daily progress is important to get the most out of the 100 Days of Code challenge, so this plugin makes it as easy as possible. Input a title, description, and resource links into the Add Log form and we will generate a log for you that includes your code metrics and milestones earned that day.
89 |
90 |
91 |
92 | ### **3. View Logs**
93 |
94 |
95 |
96 |
97 |
98 | When you submit a log, it is added to your **Logs** page. Here you can view details about each log, including title, description, links, code metrics, and milestones for that day. Each day's metrics are compared to your average metrics calculated from all your log entries throughout the challenge.
99 |
100 | If you forget to create a log for a day that you worked, don’t worry! We will automatically create a log for you. It will appear in your logs as **No Title**. You can edit these logs at any time with any additional details that you would like to add.
101 |
102 |
103 |
104 | ### **4. View Milestones**
105 |
106 | Milestones are fun, shareable badges that you can earn during the #100DaysOfCode challenge. For example, you can earn badges by coding for 10 hours, coding in a new language, or coding for 30 days in a row.
107 |
108 |
109 |
110 |
111 |
112 | To open the **Milestones** view, click **View Milestones** below the **View Logs** link in the Code Time view of your editor's sidebar.
113 |
114 | Milestones are divided into six levels of difficulty. You should complete the level one Milestones within the first few days, the level five Milestones should be completed by the end of the challenge, and the level 6 Milestones are for those who are looking for a little more when the challenge has been completed.
115 |
116 |
117 |
118 | ### **5. View Dashboard**
119 |
120 | The **Dashboard** helps you visualize your progress during the challenge. You can view your aggregated code metrics for the challenge, your most recent Logs and Milestones, and a graph of your code time throughout the challenge.
121 |
122 | Aggregated metrics at the top of your Dashboard help you measure your progress toward key milestones. Progress bars show how close you are to achieving each milestone and turn gold when you have reached your goal.
123 |
124 |
125 |
126 |
127 |
128 | To navigate to the dashboard, click the **View Dashboard** button in the 100 Days of Code section of the Code Time view.
129 |
130 |
131 |
132 | ### **6. Share your progress**
133 |
134 | To be a part of the community, it's important to share your progress during the 100 Days of Code challenge. Even minor progress is progress, and we encourage you to share something every day.
135 |
136 |
137 |
138 |
139 |
140 | Each log entry has a share button that will allow you to directly tweet your progress along with your daily code time metrics. You can also share any of your milestones right from your code editor.
141 |
142 |
143 |
144 |
145 |
146 | Tweets about your milestones will include a badge, description, and link for your followers to explore.
147 |
148 |
149 |
150 | ### **7. Add this badge to your work**
151 |
152 | As a participant in the 100 Days of Code challenge, you will work on a lot of projects. To show your followers that you have joined the 100 Days of Code challenge, you can add a custom 100 Days of Code badge to your repositories, website, or GitHub profile README.
153 |
154 |
155 |
156 |
157 |
158 |
159 | You can add it to a Markdown file:
160 |
161 | ```markdown
162 | Left badge:
163 |
164 | [](https://www.software.com/100-days-of-code)
165 |
166 | Right badge:
167 |
168 | [](https://www.100daysofcode.com)
169 | ```
170 |
171 | Or you can add it to an HTML file:
172 |
173 | ```html
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
186 |
187 | ```
188 |
189 |
190 |
191 | ### **8. Earn a 100 Days of Code Certificate**
192 |
193 | Yay! You completed the challenge. At the end of the 100 Days, this 100 Days of Code certificate will be unlocked on your dashboard. Share your achievement with the community you've been a part of on Twitter, LinkedIn and other social media websites.
194 |
195 |
196 |
197 |
198 |
199 | ## 100 Days of Code Resources
200 |
201 | Need help getting started? Check out some of these resources we've gathered to help you complete your 100 Days of Code challenge.
202 |
203 | - [Essential Guide to the 100 Days of Code Challenge](https://www.software.com/src/essential-guide-to-the-100-days-of-code-challenge) - _Software_
204 | - [#100DaysofCode Official Website](https://www.100daysofcode.com/) - _#100DaysofCode_
205 | - [Improve with the #100DaysOfCode Movement: Rounds, Resistance, and Adaptation](https://www.freecodecamp.org/news/the-100daysofcode-movement-rounds-resistance-and-adaptation-432429cc3306/) - _freeCodeCamp_
206 | - [How to set up your own personal website for 100 Days of Code](https://www.software.com/src/how-to-set-up-your-own-personal-website-for-100-days-of-code) - _Software_
207 |
208 | For a **full** list of the most important communities, articles, podcasts, and tools, read our [Essential Guide to the 100 Days of Code Challenge](https://www.software.com/src/essential-guide-to-the-100-days-of-code-challenge).
209 |
210 |
211 |
212 | ## What's next?
213 |
214 | The 100 Days of Code challenge is the first step in becoming a better, more consistent developer. Once you complete the challenge, how can you maintain your momentum?
215 |
216 | Consider starting a second round of 100 days. Pick a new language or framework to learn, and level up even faster.
217 |
218 | You can also continue to use [Code Time](https://www.software.com/code-time), our time tracking extension that powers the 100 Days of Code plugin. With Code Time, you can track your progress, set goals, view your coding stats, and sign up for weekly email reports.
219 |
220 | You can also view all your coding metrics right in your feed at [app.software.com](https://app.software.com).
221 |
222 |
223 |
224 | ## Contributing & feedback
225 |
226 | Enjoying the 100 Days of Code plugin? Let us know how it’s going by tweeting or following us at [@software_hq](https://twitter.com/software_hq).
227 |
228 | Find a bug or have an idea for a new feature? You can open an issue on our [GitHub repo](https://github.com/swdotcom/swdc-vscode-100-days-of-code/tree/develop) or contact us at [support@software.com](mailto:support@software.com) with any additional questions or feedback.
229 |
--------------------------------------------------------------------------------
/assets/templates/addLog.template.html:
--------------------------------------------------------------------------------
1 |
2 |
92 |
93 |
94 |
Log Today's Progress
95 |
Day ${this.day} | ${this.month} ${this.date}, ${this.year}
96 |
Title
97 |
98 |
Description
99 |
100 |
101 |
102 | Link(s) to Today's Work (Separate links with commas)
103 |
104 |
110 |
111 |
Hours coded
112 |
113 |
hours
114 |
${this.keystrokes}
115 |
${this.linesAdded}
116 |
117 | You’ve logged ${this.hours} hours, ${this.keystrokes} keystrokes, and ${this.linesAdded} lines of code
118 | so far today based on our Code Time plugin.
119 |
120 |
121 | Submit
122 | Cancel
123 |
124 |
125 |
126 |
170 |
171 |
--------------------------------------------------------------------------------
/assets/templates/dashboard.template.html:
--------------------------------------------------------------------------------
1 |
2 |
397 |
398 |
399 |
100 Days of Code Dashboard
400 |
401 |
402 |
406 |
407 |
408 |
The 100 Days of Code extension is no longer being maintained.
409 |
You can continue to track your progress towards completing the challenge using
Code Time
410 |
411 |
412 |
413 |
414 | You must log in with Code Time to start tracking your 100 Days of Code
415 |
416 |
417 |
${this.days}
418 |
days coded
419 |
426 |
${this.daysLevelTooltip}
427 |
428 |
429 |
${this.hours}
430 |
hours coded
431 |
438 |
${this.hoursLevelTooltip}
439 |
440 |
441 |
${this.streaks}
442 |
longest streak
443 |
450 |
${this.streaksLevelTooltip}
451 |
452 |
453 |
${this.linesAdded}
454 |
lines added
455 |
462 |
${this.linesAddedLevelTooltip}
463 |
464 |
465 |
${this.avgHours}
466 |
avg hours/day
467 |
474 |
${this.avgHoursLevelTooltip}
475 |
476 |
477 |
478 |
479 |
480 |
Logs
481 |
View logs
482 |
483 | Day
484 | Date
485 | Subject
486 |
487 | ${this.logsHtml}
488 |
489 |
490 |
491 |
492 |
Recent Milestones
493 |
View milestones
494 |
${this.milestoneHtml}
495 |
496 |
497 |
498 |
499 |
Days Coded: ${this.days} days
500 |
501 |
${this.min} hr
502 |
503 |
${this.mid} hr
504 |
505 |
${this.max} hr
506 |
${this.barsHtml}
507 |
${this.xAxisDates}
508 |
509 |
510 |
511 |
512 |
513 |
Current challenge round
514 |
${this.currentChallengeRound}
515 |
516 |
517 |
518 |
519 |
520 |
521 |
558 |
559 |
--------------------------------------------------------------------------------
/assets/templates/milestones.template.html:
--------------------------------------------------------------------------------
1 |
2 |
183 |
184 |
185 |
Milestones
186 |
188 |
190 |
193 |
194 |
195 |
196 |
The 100 Days of Code extension is no longer being maintained.
197 |
You can continue to track your progress towards completing the challenge using
Code Time
198 |
199 |
200 |
201 |
202 | You must log in with Code Time to start tracking your 100 Days of Code
203 |
204 | ${this.recents} ${this.allMilestones}
205 |
206 |
233 |
234 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swdc-100-days-of-code",
3 | "displayName": "100 Days of Code",
4 | "description": "100 Days of Code helps you reach your goals and complete the #100DaysOfCode Challenge.",
5 | "version": "1.2.1",
6 | "publisher": "softwaredotcom",
7 | "icon": "resources/100-days-of-code-icon.png",
8 | "scripts": {
9 | "compile": "tsc -p ./",
10 | "lint": "tslint -p ./",
11 | "watch": "tsc -watch -p ./",
12 | "build": "vsce package --yarn",
13 | "vscode:prepublish": "webpack --mode production",
14 | "webpack": "webpack --mode development",
15 | "webpack-dev": "webpack --mode development --watch",
16 | "test-compile": "tsc -p ./",
17 | "pretest": "yarn run compile && yarn run lint",
18 | "test": "node ./out/test/runTest.js"
19 | },
20 | "extensionKind": [
21 | "ui",
22 | "workspace"
23 | ],
24 | "engines": {
25 | "vscode": "^1.52.0"
26 | },
27 | "preview": false,
28 | "categories": [
29 | "Other"
30 | ],
31 | "galleryBanner": {
32 | "color": "#384356",
33 | "theme": "dark"
34 | },
35 | "repository": {
36 | "type": "git",
37 | "url": "https://github.com/swdotcom/swdc-vscode-100-days-of-code"
38 | },
39 | "license": "SEE LICENSE IN LICENSE",
40 | "keywords": [
41 | "productivity",
42 | "100 days of code",
43 | "code time",
44 | "challenge",
45 | "time tracking"
46 | ],
47 | "activationEvents": [
48 | "onStartupFinished"
49 | ],
50 | "main": "./dist/extension",
51 | "contributes": {
52 | "commands": [
53 | {
54 | "command": "DoC.ViewReadme",
55 | "title": "100 Days of Code: View Readme"
56 | },
57 | {
58 | "command": "DoC.viewLogs",
59 | "title": "100 Days of Code: View Logs"
60 | },
61 | {
62 | "command": "DoC.addLog",
63 | "title": "100 Days of Code: Add Daily Progress Log"
64 | },
65 | {
66 | "command": "DoC.viewDashboard",
67 | "title": "100 Days of Code: View Dashboard"
68 | },
69 | {
70 | "command": "DoC.viewMilestones",
71 | "title": "100 Days of Code: View Milestones"
72 | }
73 | ],
74 | "viewsContainers": {
75 | "activitybar": [
76 | {
77 | "id": "100-days-of-code",
78 | "title": "100 Days of Code",
79 | "icon": "resources/dark/milestones.svg"
80 | }
81 | ]
82 | },
83 | "views": {
84 | "100-days-of-code": [
85 | {
86 | "id": "100-days-of-code-view",
87 | "name": ""
88 | }
89 | ]
90 | }
91 | },
92 | "devDependencies": {
93 | "@types/glob": "^7.1.1",
94 | "@types/mocha": "^7.0.1",
95 | "@types/node": "^12.11.7",
96 | "@types/vscode": "^1.35.0",
97 | "@typescript-eslint/eslint-plugin": "^2.18.0",
98 | "@typescript-eslint/parser": "^2.18.0",
99 | "copy-webpack-plugin": "^7.0.0",
100 | "eslint": "^6.8.0",
101 | "glob": "^7.1.6",
102 | "mocha": "^7.0.1",
103 | "ts-loader": "^8.0.12",
104 | "typescript": "^3.7.5",
105 | "vscode-test": "^1.3.0",
106 | "webpack": "^5.10.1",
107 | "webpack-cli": "^4.2.0"
108 | },
109 | "dependencies": {
110 | "axios": "^0.21.1",
111 | "file-it": "^1.0.26",
112 | "moment-timezone": "^0.5.28",
113 | "open": "^7.0.3",
114 | "query-string": "^6.13.7",
115 | "swdc-tracker": "^1.3.3"
116 | },
117 | "extensionDependencies": [
118 | "softwaredotcom.swdc-vscode"
119 | ]
120 | }
121 |
--------------------------------------------------------------------------------
/resources/100-days-of-code-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode-100-days-of-code/5e74bb622660543701e05dbfb37408a504e95d9c/resources/100-days-of-code-icon.png
--------------------------------------------------------------------------------
/resources/dark/dashboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/dark/learn-more.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/dark/logs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/dark/milestones.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/dark/refresh copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/dark/refresh-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/dark/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/light/dashboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/light/learn-more.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/light/logs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/light/milestones.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/light/refresh-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/light/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode-100-days-of-code/5e74bb622660543701e05dbfb37408a504e95d9c/resources/light/refresh.png
--------------------------------------------------------------------------------
/resources/light/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | // The module 'vscode' contains the VS Code extensibility API
2 | // Import the module and reference it with the alias vscode in your code below
3 | import * as vscode from "vscode";
4 | import { createCommands } from "./utils/CommandUtil";
5 | import { checkMilestonesJson } from "./utils/MilestonesUtil";
6 | import { syncLogs } from "./utils/LogsUtil";
7 | import { displayReadmeIfNotExists, isLoggedIn, getMillisSinceLastUpdate } from "./utils/Util";
8 | import { getPluginName, getVersion } from "./utils/PluginUtil";
9 | import { MilestoneEventManager } from "./managers/MilestoneEventManager";
10 | import { getSummaryJsonFilePath } from "./managers/FileManager";
11 |
12 | const thirty_seconds = 1000 * 30;
13 | let milestoneMgr: MilestoneEventManager;
14 |
15 | // this method is called when the extension is activated
16 | export function activate(ctx: vscode.ExtensionContext) {
17 | // Use the console to output diagnostic information (console.log) and errors (console.error)
18 | // This line of code will only be executed once when your extension is activated
19 | console.log(`Loaded ${getPluginName()} v${getVersion()}`);
20 |
21 | // Initialize all the files and db setup for the plugin
22 | initializePlugin();
23 |
24 | // adding commands for 100 Days of code pages and events
25 | ctx.subscriptions.push(createCommands());
26 | }
27 |
28 | export async function initializePlugin() {
29 | // checks if all the files exist
30 | checkMilestonesJson();
31 |
32 | // Displays README on first launch
33 | displayReadmeIfNotExists();
34 |
35 | // this needs to be done on any parent or child window to initiate the file event listening logic
36 | milestoneMgr = MilestoneEventManager.getInstance();
37 |
38 | // initialize logs, summary, and milestone data if its the primary window
39 | if (isLoggedIn()) {
40 | // This is important to initialize in order to obtain the challenge round
41 | // if its not already in memory
42 | const millisSinceUpdate = getMillisSinceLastUpdate(getSummaryJsonFilePath());
43 | if (isThresholdMet(millisSinceUpdate)) {
44 | // initialize the user summary info (challenge_round and metrics data)
45 | await syncLogs();
46 | await milestoneMgr.fetchAllMilestones();
47 | }
48 | }
49 | }
50 |
51 | function isThresholdMet(millisSinceUpdate) {
52 | return millisSinceUpdate === -1 || millisSinceUpdate > thirty_seconds;
53 | }
54 |
55 | export function deactivate(ctx: vscode.ExtensionContext) {
56 | // add deactivate functionality here
57 | if (milestoneMgr) {
58 | milestoneMgr.dispose();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/managers/FileManager.ts:
--------------------------------------------------------------------------------
1 | import { getSoftwareDir, isWindows } from "../utils/Util";
2 | import fs = require("fs");
3 | import { Summary } from "../models/Summary";
4 |
5 | export function getFile(name: string) {
6 | let file_path = getSoftwareDir();
7 | if (isWindows()) {
8 | return `${file_path}\\${name}`;
9 | }
10 | return `${file_path}/${name}`;
11 | }
12 |
13 | export function getFileDataAsJson(filepath: string): any {
14 | if (fs.existsSync(filepath)) {
15 | try {
16 | const content: string = fs.readFileSync(filepath, "utf-8");
17 | return JSON.parse(content);
18 | } catch (e) {
19 | console.log("File not found: " + filepath);
20 | }
21 | }
22 | return null;
23 | }
24 |
25 | export function getSummaryJsonFilePath() {
26 | return getFile("userSummary.json");
27 | }
28 |
29 | export function fetchSummaryJsonFileData(): Summary {
30 | const emptySummary:Summary = new Summary();
31 | // checks if summary JSON exists. If not populates it with base values
32 | const filepath = getSummaryJsonFilePath();
33 | if (!fs.existsSync(filepath)) {
34 | // create a blank summary
35 | fs.writeFileSync(filepath, [JSON.stringify(emptySummary, null, 2)]);
36 | }
37 |
38 | try {
39 | // return the local summary
40 | return getFileDataAsJson(filepath);
41 | } catch (e) {
42 | console.log("File not found: " + filepath);
43 | }
44 | return emptySummary;
45 | }
46 |
--------------------------------------------------------------------------------
/src/managers/MilestoneEventManager.ts:
--------------------------------------------------------------------------------
1 | import { window, WindowState } from "vscode";
2 | import {
3 | checkCodeTimeMetricsMilestonesAchieved,
4 | checkDaysMilestones,
5 | checkLanguageMilestonesAchieved,
6 | compareWithLocalMilestones,
7 | getTodaysLocalMilestones,
8 | } from "../utils/MilestonesUtil";
9 | import { syncSummary } from "../utils/SummaryUtil";
10 | import { getItem, isLoggedIn, setItem } from "../utils/Util";
11 |
12 | let milestoneTimer: NodeJS.Timer = undefined;
13 | const MILESTONE_CHECK_THESHOLD = 1000 * 60 * 10;
14 |
15 | export class MilestoneEventManager {
16 | private static instance: MilestoneEventManager;
17 |
18 | private _checkingForMilestones: boolean = false;
19 |
20 | static getInstance(): MilestoneEventManager {
21 | if (!MilestoneEventManager.instance) {
22 | MilestoneEventManager.instance = new MilestoneEventManager();
23 | }
24 |
25 | return MilestoneEventManager.instance;
26 | }
27 |
28 | private constructor() {
29 | if (!milestoneTimer) {
30 | milestoneTimer = setInterval(() => {
31 | this.checkForMilestones();
32 | }, 1000 * 60 * 3);
33 |
34 | window.onDidChangeWindowState(this._windowStateChanged, this);
35 | }
36 | }
37 |
38 | private _windowStateChanged(winState: WindowState) {
39 | if (winState.focused) {
40 | const now = new Date().getTime();
41 | const lastTimeChecked = getItem("last100doc_milestoneCheckTime") ?? 0;
42 | const passedThreshold = !!(now - lastTimeChecked >= MILESTONE_CHECK_THESHOLD);
43 | if (passedThreshold) {
44 | this.checkForMilestones();
45 | }
46 | }
47 | }
48 |
49 | public dispose() {
50 | if (milestoneTimer) {
51 | clearInterval(milestoneTimer);
52 | milestoneTimer = null;
53 | }
54 | }
55 |
56 | private async checkForMilestones() {
57 | if (!isLoggedIn() || this._checkingForMilestones || !window.state.focused) {
58 | return;
59 | }
60 | const now = new Date().getTime();
61 | setItem("last100doc_milestoneCheckTime", now);
62 | this._checkingForMilestones = true;
63 | // updates logs with latest metrics and checks for milestones
64 |
65 | // checks to see if there are any new achieved milestones
66 | const achievedTimeMilestones = checkCodeTimeMetricsMilestonesAchieved();
67 |
68 | // checks to see if there are any new language milestones achieved
69 | const achievedLangMilestones = checkLanguageMilestonesAchieved();
70 |
71 | // checks to see if there are any day milestones achived
72 | const achievedDaysMilestones = checkDaysMilestones();
73 |
74 | if (achievedDaysMilestones.length || achievedLangMilestones.length || achievedTimeMilestones.length) {
75 | // get the current milestones so if its an update, the milestone array
76 | // has all milestones for this day instead of getting replaced by a new set of milestones
77 | const currentMilestones: Array = getTodaysLocalMilestones() || [];
78 |
79 | // fetch the milestones
80 | await this.fetchMilestones();
81 |
82 | // syncs the Summary info (hours, lines, etc) to the file
83 | syncSummary();
84 | }
85 | this._checkingForMilestones = false;
86 | }
87 |
88 | /**
89 | * This will return an array of..
90 | * [{challenge_round, createdAt, day_number, local_date, milestones [numbers], offset_minutes, timezone, type, unix_date, userId}]
91 | * @param date
92 | */
93 | public async fetchMilestones(date: any = null): Promise {
94 | const ONE_DAY_SEC = 68400000;
95 | // default to today and yesterday
96 | let endDate = new Date(); // 11:59:59 pm today
97 | let startDate = new Date(endDate.valueOf() - ONE_DAY_SEC * 2); // 12:00:01 am yesterday
98 |
99 | if (date) {
100 | endDate = new Date(date); // 11:59:59 pm today
101 | startDate = new Date(endDate.valueOf() - ONE_DAY_SEC); // 12:00:01 am yesterday
102 | }
103 | // normalize dates
104 | startDate.setHours(0, 0, 1, 0);
105 | endDate.setHours(23, 59, 59, 0);
106 |
107 | const milestoneData = [];
108 |
109 | // sync with local
110 | if (milestoneData) {
111 | compareWithLocalMilestones(milestoneData);
112 | }
113 |
114 | // return milestones
115 | return milestoneData;
116 | }
117 |
118 | /**
119 | * This will return an array of..
120 | * [{challenge_round, createdAt, day_number, local_date, milestones [numbers], offset_minutes, timezone, type, unix_date, userId}]
121 | * @param date a date value (timestamp or date string)
122 | */
123 | public async fetchAllMilestones(): Promise {
124 |
125 | const milestoneData = [];
126 |
127 | // sync with local
128 | if (milestoneData) {
129 | compareWithLocalMilestones(milestoneData);
130 | }
131 |
132 | // return milestones
133 | return milestoneData;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/models/CodetimeMetrics.ts:
--------------------------------------------------------------------------------
1 | export class CodetimeMetrics {
2 | public hours: number = 0;
3 | public lines_added: number = 0;
4 | public keystrokes: number = 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/models/Log.ts:
--------------------------------------------------------------------------------
1 | import { CodetimeMetrics } from "./CodetimeMetrics";
2 | import { NO_TITLE_LABEL } from "../utils/Constants";
3 |
4 | const moment = require("moment-timezone");
5 |
6 | export class Log {
7 | public day_number: number = 0;
8 | public unix_date: number = 0;
9 | public local_date: number = 0;
10 | public date: number = moment().valueOf();
11 | public title: string = NO_TITLE_LABEL;
12 | public description: string = "";
13 | public links: Array = [];
14 | public codetime_metrics: CodetimeMetrics = new CodetimeMetrics();
15 | public shared: boolean = false;
16 | public milestones: Array = [];
17 | public challenge_round: number = 0;
18 | }
19 |
--------------------------------------------------------------------------------
/src/models/Milestone.ts:
--------------------------------------------------------------------------------
1 | export class Milestone {
2 | public id: number = 0;
3 | public title: string = "";
4 | public description: string = "Completed 1 hr of coding";
5 | public level: number = 1;
6 | public category: string = "time";
7 | public achieved: boolean = false;
8 | public date_achieved: number = 0;
9 | public shared: boolean = false;
10 | public icon: string = "";
11 | public gray_icon: string = "";
12 | }
13 |
--------------------------------------------------------------------------------
/src/models/Summary.ts:
--------------------------------------------------------------------------------
1 | export class Summary {
2 | public days: number = 0;
3 | public currentDate: number = 0;
4 | public currentHours: number = 0;
5 | public currentKeystrokes: number = 0;
6 | public currentLines: number = 0;
7 | public hours: number = 0;
8 | public longest_streak: number = 0;
9 | public milestones: number = 0;
10 | public lines_added: number = 0;
11 | public keystrokes: number = 0;
12 | public recent_milestones: number[] = [];
13 | public current_streak: number = 0;
14 | public shares: number = 0;
15 | public languages: string[] = [];
16 | public lastUpdated: number = 0;
17 | public challenge_round: number = 1;
18 | }
19 |
--------------------------------------------------------------------------------
/src/models/TreeNode.ts:
--------------------------------------------------------------------------------
1 | import { TreeItemCollapsibleState } from "vscode";
2 |
3 | export class TreeNode {
4 | public id: string = "";
5 | public label: string = "";
6 | public value: any = null;
7 | public description: string = "";
8 | public tooltip: string = "";
9 | public command: string = "";
10 | public commandArgs: any[] = [];
11 | public type: string = "";
12 | public contextValue: string = "";
13 | public callback: any = null;
14 | public icon: string = "";
15 | public children: TreeNode[] = [];
16 | public initialCollapsibleState: TreeItemCollapsibleState = TreeItemCollapsibleState.Collapsed;
17 | public element_name: string = "";
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/runTest.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 |
3 | import { runTests } from 'vscode-test';
4 |
5 | async function main() {
6 | try {
7 | // The folder containing the Extension Manifest package.json
8 | // Passed to `--extensionDevelopmentPath`
9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../');
10 |
11 | // The path to test runner
12 | // Passed to --extensionTestsPath
13 | const extensionTestsPath = path.resolve(__dirname, './suite/index');
14 |
15 | // Download VS Code, unzip it and run the integration test
16 | await runTests({ extensionDevelopmentPath, extensionTestsPath });
17 | } catch (err) {
18 | console.error('Failed to run tests');
19 | process.exit(1);
20 | }
21 | }
22 |
23 | main();
24 |
--------------------------------------------------------------------------------
/src/test/suite/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from "assert";
2 |
3 | // You can import and use all API from the 'vscode' module
4 | // as well as import your extension to test it
5 | import * as vscode from "vscode";
6 | // import * as myExtension from '../extension';
7 |
8 | suite("Extension Test Suite", () => {
9 | vscode.window.showInformationMessage("Start all tests.");
10 |
11 | test("Sample test", () => {
12 | assert.equal(-1, [1, 2, 3].indexOf(5));
13 | assert.equal(-1, [1, 2, 3].indexOf(0));
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/test/suite/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as Mocha from 'mocha';
3 | import * as glob from 'glob';
4 |
5 | export function run(): Promise {
6 | // Create the mocha test
7 | const mocha = new Mocha({
8 | ui: 'tdd',
9 | });
10 | mocha.useColors(true);
11 |
12 | const testsRoot = path.resolve(__dirname, '..');
13 |
14 | return new Promise((c, e) => {
15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
16 | if (err) {
17 | return e(err);
18 | }
19 |
20 | // Add files to the test suite
21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
22 |
23 | try {
24 | // Run the mocha test
25 | mocha.run(failures => {
26 | if (failures > 0) {
27 | e(new Error(`${failures} tests failed.`));
28 | } else {
29 | c();
30 | }
31 | });
32 | } catch (err) {
33 | console.error(err);
34 | e(err);
35 | }
36 | });
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/tree/DoCTreeItem.ts:
--------------------------------------------------------------------------------
1 | import { TreeItem, TreeItemCollapsibleState, Command } from "vscode";
2 | import * as path from "path";
3 | import { TreeNode } from "../models/TreeNode";
4 |
5 | const resourcePath: string = path.join(__dirname, "resources");
6 |
7 | export class DoCTreeItem extends TreeItem {
8 | constructor(
9 | private readonly treeItem: TreeNode,
10 | public readonly collapsibleState: TreeItemCollapsibleState,
11 | public readonly command?: Command
12 | ) {
13 | super(treeItem.label, collapsibleState);
14 |
15 | // get the description (sub label)
16 | this.description = treeItem.description;
17 |
18 | // get the icons
19 | const { lightPath, darkPath } = getTreeItemIcon(treeItem);
20 | if (lightPath && darkPath) {
21 | this.iconPath.light = lightPath;
22 | this.iconPath.dark = darkPath;
23 | } else {
24 | // no icon found, delete it
25 | delete this.iconPath;
26 | }
27 |
28 | // set the context value (used in the config to match for icon hovering)
29 | this.contextValue = getTreeItemContextValue(treeItem);
30 | }
31 |
32 | iconPath = {
33 | light: "",
34 | dark: ""
35 | };
36 |
37 | contextValue = "treeItem";
38 | }
39 |
40 | function getTreeItemIcon(treeItem: TreeNode): any {
41 | const iconName = treeItem.icon;
42 | const lightPath = iconName && treeItem.children.length === 0 ? path.join(resourcePath, "light", iconName) : null;
43 | const darkPath = iconName && treeItem.children.length === 0 ? path.join(resourcePath, "dark", iconName) : null;
44 | return { lightPath, darkPath };
45 | }
46 |
47 | function getTreeItemContextValue(treeItem: TreeNode): string {
48 | if (treeItem.contextValue) {
49 | return treeItem.contextValue;
50 | }
51 | if (treeItem.children.length) {
52 | return "parent";
53 | }
54 | return "child";
55 | }
56 |
--------------------------------------------------------------------------------
/src/tree/Tree100DoCProvider.ts:
--------------------------------------------------------------------------------
1 | import {
2 | TreeDataProvider,
3 | TreeItemCollapsibleState,
4 | EventEmitter,
5 | Event,
6 | Disposable,
7 | TreeView,
8 | commands
9 | } from "vscode";
10 | import { TreeNode } from "../models/TreeNode";
11 | import { DoCTreeItem } from "./DoCTreeItem";
12 | import {
13 | getDoCLearnMoreButton,
14 | getDocMilestonesButton,
15 | getDoCLogsButon,
16 | getDashboardButton,
17 | getChallengeRoundRestartButton
18 | } from "./TreeButtonManager";
19 |
20 | const docCollapsedStateMap: any = {};
21 |
22 | export const connectDoCTreeView = (view: TreeView) => {
23 |
24 | return Disposable.from(
25 | view.onDidCollapseElement(async e => {
26 | const item: TreeNode = e.element;
27 | docCollapsedStateMap[item.label] = TreeItemCollapsibleState.Collapsed;
28 | }),
29 |
30 | view.onDidExpandElement(async e => {
31 | const item: TreeNode = e.element;
32 | docCollapsedStateMap[item.label] = TreeItemCollapsibleState.Expanded;
33 | }),
34 |
35 | view.onDidChangeSelection(async e => {
36 | if (!e.selection || e.selection.length === 0) {
37 | return;
38 | }
39 |
40 | const item: TreeNode = e.selection[0];
41 | if (item.command) {
42 | const args = item.commandArgs || null;
43 | if (args) {
44 | commands.executeCommand(item.command, ...args);
45 | } else {
46 | // run the command
47 | commands.executeCommand(item.command);
48 | }
49 | }
50 | }),
51 |
52 | view.onDidChangeVisibility(e => {
53 | if (e.visible) {
54 | //
55 | }
56 | })
57 | );
58 | };
59 |
60 | export class Tree100DoCProvider implements TreeDataProvider {
61 | private _onDidChangeTreeData: EventEmitter = new EventEmitter();
62 |
63 | readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event;
64 |
65 | private view: TreeView | undefined;
66 | private initializedTree: boolean = false;
67 |
68 | constructor() {
69 | //
70 | }
71 |
72 | async revealTree() {
73 | if (!this.initializedTree) {
74 | await this.refresh();
75 | }
76 |
77 | setTimeout(() => {
78 | const learnMoreButton: TreeNode = getDoCLearnMoreButton();
79 | try {
80 | if (this.view) {
81 | // select the readme item
82 | this.view.reveal(learnMoreButton, {
83 | focus: true,
84 | select: false
85 | });
86 | }
87 | } catch (err) {
88 | console.log(`Unable to select tree item: ${err.message}`);
89 | }
90 | }, 1000);
91 | }
92 |
93 | bindView(docTreeView: TreeView): void {
94 | this.view = docTreeView;
95 | }
96 |
97 | getParent(_p: TreeNode) {
98 | return void 0; // all playlists are in root
99 | }
100 |
101 | refresh(): void {
102 | this._onDidChangeTreeData.fire(undefined);
103 | }
104 |
105 | refreshParent(parent: TreeNode) {
106 | this._onDidChangeTreeData.fire(parent);
107 | }
108 |
109 | getTreeItem(p: TreeNode): DoCTreeItem {
110 | let treeItem: DoCTreeItem;
111 | if (p.children.length) {
112 | let collapsibleState = docCollapsedStateMap[p.label];
113 | if (!collapsibleState) {
114 | treeItem = createDoCTreeItem(p, p.initialCollapsibleState);
115 | } else {
116 | treeItem = createDoCTreeItem(p, collapsibleState);
117 | }
118 | } else {
119 | treeItem = createDoCTreeItem(p, TreeItemCollapsibleState.None);
120 | }
121 |
122 | return treeItem;
123 | }
124 |
125 | async getChildren(element?: TreeNode): Promise {
126 | let nodeItems: TreeNode[] = [];
127 |
128 | if (element) {
129 | // return the children of this element
130 | nodeItems = element.children;
131 | } else {
132 | // return the parent elements
133 | nodeItems = await this.getMenuParents();
134 | }
135 | return nodeItems;
136 | }
137 |
138 | async getMenuParents(): Promise {
139 | const treeItems: TreeNode[] = [];
140 |
141 | const feedbackButton: TreeNode = getDoCLearnMoreButton();
142 | treeItems.push(feedbackButton);
143 |
144 | // get the manage bookmarks button
145 |
146 | const dashboardButton: TreeNode = getDashboardButton();
147 | treeItems.push(dashboardButton);
148 |
149 | const logsButton: TreeNode = getDoCLogsButon();
150 | treeItems.push(logsButton);
151 |
152 | const milestoneButton: TreeNode = getDocMilestonesButton();
153 | treeItems.push(milestoneButton);
154 |
155 | const challengeRestartButton: TreeNode = getChallengeRoundRestartButton();
156 | treeItems.push(challengeRestartButton);
157 |
158 | return treeItems;
159 | }
160 | }
161 |
162 | /**
163 | * Create the playlist tree item (root or leaf)
164 | * @param p
165 | * @param cstate
166 | */
167 | function createDoCTreeItem(p: TreeNode, cstate: TreeItemCollapsibleState) {
168 | return new DoCTreeItem(p, cstate);
169 | }
170 |
--------------------------------------------------------------------------------
/src/tree/TreeButtonManager.ts:
--------------------------------------------------------------------------------
1 | import { TreeNode } from "../models/TreeNode";
2 |
3 | export function getActionButton(
4 | label: string,
5 | tooltip: string,
6 | command: string,
7 | icon: any = null,
8 | element_name: string = ""
9 | ): TreeNode {
10 | const item: TreeNode = new TreeNode();
11 | item.tooltip = tooltip;
12 | item.label = label;
13 | item.id = label;
14 | item.command = command;
15 | item.icon = icon;
16 | item.contextValue = "action_button";
17 | item.element_name = element_name;
18 | return item;
19 | }
20 |
21 | export function getDoCLearnMoreButton() {
22 | return getActionButton(
23 | "Learn more",
24 | "View 100 Days of Code Readme to learn more",
25 | "DoC.ViewReadme",
26 | "learn-more.svg",
27 | "100doc_learn_more_btn"
28 | );
29 | }
30 |
31 | export function getDashboardButton() {
32 | return getActionButton(
33 | "View dashboard",
34 | "View 100 Days of Code dashboard",
35 | "DoC.viewDashboard",
36 | "dashboard.svg",
37 | "100doc_dashboard_btn"
38 | );
39 | }
40 |
41 | export function getDoCLogsButon() {
42 | return getActionButton(
43 | "View logs",
44 | "View 100 Days of Code log entries",
45 | "DoC.viewLogs",
46 | "logs.svg",
47 | "100doc_logs_btn"
48 | );
49 | }
50 |
51 | export function getDocMilestonesButton() {
52 | return getActionButton(
53 | "View milestones",
54 | "View 100 Days of Code milestones",
55 | "DoC.viewMilestones",
56 | "milestones.svg",
57 | "100doc_milestones_btn"
58 | );
59 | }
60 |
61 | export function getChallengeRoundRestartButton() {
62 | return getActionButton(
63 | "Restart challenge",
64 | "Restart the challenge round",
65 | "DoC.restartChallengeRound",
66 | "refresh.svg",
67 | "100doc_learn_more_btn"
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils/AddLogUtil.ts:
--------------------------------------------------------------------------------
1 | import path = require("path");
2 | import fs = require("fs");
3 | import { getDayNumberForNewLog } from "./LogsUtil";
4 | import { getSessionCodetimeMetrics } from "./MetricUtil";
5 | import { monthNames } from "./Constants";
6 | import { getInputFormStyles } from "./Util";
7 |
8 | function getAddLogTemplate(): string {
9 | return path.join(__dirname, "/assets/templates/addLog.template.html");
10 | }
11 |
12 | export function getAddLogHtmlString(): string {
13 | const dateOb = new Date();
14 | const date = dateOb.getDate();
15 | const month = monthNames[dateOb.getMonth()]; // Month is 0 indexed
16 | const year = dateOb.getFullYear();
17 | let day = getDayNumberForNewLog();
18 |
19 | // metrics is stored as [minutes, keystrokes, lines]
20 | const metrics = getSessionCodetimeMetrics();
21 | const hours = (metrics.minutes / 60).toFixed(1);
22 | const keystrokes = metrics.keystrokes;
23 | const linesAdded = metrics.linesAdded;
24 | const { cardTextColor, cardBackgroundColor, cardGrayedLevel, sharePath } = getInputFormStyles();
25 |
26 | const templateVars = {
27 | cardTextColor,
28 | cardBackgroundColor,
29 | cardGrayedLevel,
30 | sharePath,
31 | date,
32 | month,
33 | year,
34 | day,
35 | hours,
36 | keystrokes,
37 | linesAdded
38 | };
39 |
40 | const templateString = fs.readFileSync(getAddLogTemplate()).toString();
41 | const fillTemplate = function (templateString: string, templateVars: any) {
42 | return new Function("return `" + templateString + "`;").call(templateVars);
43 | };
44 |
45 | const addLogHtmlContent = fillTemplate(templateString, templateVars);
46 | return addLogHtmlContent;
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/CommandUtil.ts:
--------------------------------------------------------------------------------
1 | import { Disposable, commands, window, TreeView, ViewColumn, WebviewPanel } from "vscode";
2 | import { TreeNode } from "../models/TreeNode";
3 | import { Tree100DoCProvider, connectDoCTreeView } from "../tree/Tree100DoCProvider";
4 | import { addLogToJson, editLogEntry, updateLogShare } from "./LogsUtil";
5 | import {
6 | checkDaysMilestones,
7 | checkLanguageMilestonesAchieved,
8 | checkCodeTimeMetricsMilestonesAchieved,
9 | updateMilestoneShare,
10 | checkSharesMilestones,
11 | } from "./MilestonesUtil";
12 | import { getAddLogHtmlString } from "./AddLogUtil";
13 | import { getUpdatedDashboardHtmlString, getCertificateHtmlString } from "./DashboardUtil";
14 | import { displayReadmeIfNotExists, displayLoginPromptIfNotLoggedIn, isLoggedIn, checkIfNameChanged } from "./Util";
15 | import { getUpdatedMilestonesHtmlString } from "./MilestonesTemplateUtil";
16 | import { getUpdatedLogsHtml } from "./LogsTemplateUtil";
17 | import { deleteLogDay, syncLogs } from "./LogsUtil";
18 | import { MilestoneEventManager } from "../managers/MilestoneEventManager";
19 | import { YES_LABEL } from "./Constants";
20 | import { restartChallenge } from "./SummaryUtil";
21 |
22 | let currentTitle: string = "";
23 |
24 | export function reloadCurrentView() {
25 | if (currentTitle) {
26 | switch (currentTitle) {
27 | case "Logs":
28 | commands.executeCommand("DoC.viewLogs");
29 | break;
30 | case "Dashboard":
31 | commands.executeCommand("DoC.viewDashboard");
32 | break;
33 | case "Milestones":
34 | commands.executeCommand("DoC.viewMilestones");
35 | break;
36 | }
37 | }
38 | }
39 |
40 | export function createCommands(): { dispose: () => void } {
41 | let cmds: any[] = [];
42 | let currentPanel: WebviewPanel | undefined = undefined;
43 |
44 | const Doc100SftwProvider = new Tree100DoCProvider();
45 | const Doc100SftwTreeView: TreeView = window.createTreeView("100-days-of-code-view", {
46 | treeDataProvider: Doc100SftwProvider,
47 | showCollapseAll: true,
48 | });
49 | Doc100SftwProvider.bindView(Doc100SftwTreeView);
50 | cmds.push(connectDoCTreeView(Doc100SftwTreeView));
51 |
52 | cmds.push(
53 | commands.registerCommand("DoC.ViewReadme", () => {
54 | displayReadmeIfNotExists(true);
55 | })
56 | );
57 |
58 | cmds.push(
59 | commands.registerCommand("DoC.revealTree", () => {
60 | Doc100SftwProvider.revealTree();
61 | })
62 | );
63 |
64 | cmds.push(
65 | commands.registerCommand("DoC.viewLogs", async () => {
66 | // check if the user has changed accounts
67 | await checkIfNameChanged();
68 |
69 | const generatedHtml = getUpdatedLogsHtml();
70 |
71 | const title = "Logs";
72 | if (currentPanel && title !== currentTitle) {
73 | // dipose the previous one
74 | currentPanel.dispose();
75 | }
76 | currentTitle = title;
77 |
78 | if (!currentPanel) {
79 | currentPanel = window.createWebviewPanel("100doc", title, ViewColumn.One, { enableScripts: true });
80 | currentPanel.onDidDispose(() => {
81 | currentPanel = undefined;
82 | });
83 | currentPanel.webview.onDidReceiveMessage(async (message) => {
84 | if (!isLoggedIn()) {
85 | displayLoginPromptIfNotLoggedIn();
86 | }
87 |
88 | switch (message.command) {
89 | case "editLog":
90 | const dayUpdate = message.value;
91 |
92 | await editLogEntry(
93 | parseInt(dayUpdate.day_number, 10),
94 | parseInt(dayUpdate.unix_date, 10),
95 | dayUpdate.title,
96 | dayUpdate.description,
97 | dayUpdate.links,
98 | dayUpdate.hours
99 | );
100 |
101 | await syncLogs();
102 | commands.executeCommand("DoC.viewLogs");
103 | break;
104 | case "addLog":
105 | commands.executeCommand("DoC.addLog");
106 | break;
107 | case "incrementShare":
108 | updateLogShare(message.value);
109 | checkSharesMilestones();
110 | break;
111 | case "deleteLog":
112 | const selection = await window.showInformationMessage(
113 | `Are you sure you want to delete this log, '${message.value.title}'?`,
114 | { modal: true },
115 | ...["Yes"]
116 | );
117 |
118 | if (selection && selection === "Yes") {
119 | commands.executeCommand("DoC.deleteLog", message.value.unix_date);
120 | }
121 | break;
122 | case "refreshView":
123 | // refresh the logs then show it again
124 | await syncLogs();
125 | if (currentPanel) {
126 | // dipose the previous one
127 | currentPanel.dispose();
128 | }
129 | commands.executeCommand("DoC.viewLogs");
130 | break;
131 | case "logInToAccount":
132 | commands.executeCommand("codetime.codeTimeExisting");
133 | break;
134 | }
135 | });
136 | }
137 | currentPanel.webview.html = generatedHtml;
138 | currentPanel.reveal(ViewColumn.One);
139 |
140 | displayLoginPromptIfNotLoggedIn();
141 | })
142 | );
143 |
144 | cmds.push(
145 | commands.registerCommand("DoC.deleteLog", (unix_day: number) => {
146 | // send the delete request
147 | deleteLogDay(unix_day);
148 | })
149 | );
150 |
151 | cmds.push(
152 | commands.registerCommand("DoC.viewDashboard", async () => {
153 | // check if the user has changed accounts
154 | await checkIfNameChanged();
155 |
156 | const generatedHtml = getUpdatedDashboardHtmlString();
157 |
158 | const title = "Dashboard";
159 | if (currentPanel && title !== currentTitle) {
160 | // dipose the previous one
161 | currentPanel.dispose();
162 | }
163 | currentTitle = title;
164 |
165 | if (!currentPanel) {
166 | currentPanel = window.createWebviewPanel("100doc", title, ViewColumn.One, { enableScripts: true });
167 | currentPanel.onDidDispose(() => {
168 | currentPanel = undefined;
169 | });
170 |
171 | currentPanel.webview.onDidReceiveMessage(async (message) => {
172 | switch (message.command) {
173 | case "Logs":
174 | commands.executeCommand("DoC.viewLogs");
175 | break;
176 | case "ShareProgress":
177 | break;
178 | case "Milestones":
179 | commands.executeCommand("DoC.viewMilestones");
180 | break;
181 | case "Certificate":
182 | window
183 | .showInputBox({
184 | placeHolder: "Your name",
185 | prompt: "Please enter your name for getting the certificate. Please make sure that you are connected to the internet.",
186 | })
187 | .then((text) => {
188 | if (text) {
189 | const panel = window.createWebviewPanel("Congratulations!", "Congratulations!", ViewColumn.One);
190 | panel.webview.html = getCertificateHtmlString(text);
191 | panel.reveal(ViewColumn.One);
192 | }
193 | });
194 | case "refreshView":
195 | // refresh the logs then show it again
196 | if (currentPanel) {
197 | // dipose the previous one
198 | currentPanel.dispose();
199 | }
200 | commands.executeCommand("DoC.viewDashboard");
201 | break;
202 | case "logInToAccount":
203 | commands.executeCommand("codetime.codeTimeExisting");
204 | break;
205 | }
206 | });
207 | }
208 |
209 | currentPanel.webview.html = generatedHtml;
210 | currentPanel.reveal(ViewColumn.One);
211 |
212 | displayLoginPromptIfNotLoggedIn();
213 | })
214 | );
215 |
216 | cmds.push(
217 | commands.registerCommand("DoC.viewMilestones", async () => {
218 | // check if the user has changed accounts
219 | await checkIfNameChanged();
220 |
221 | if (isLoggedIn()) {
222 | checkCodeTimeMetricsMilestonesAchieved();
223 | checkLanguageMilestonesAchieved();
224 | checkDaysMilestones();
225 | }
226 |
227 | const generatedHtml = getUpdatedMilestonesHtmlString();
228 |
229 | const title = "Milestones";
230 | if (currentPanel && title !== currentTitle) {
231 | // dipose the previous one
232 | currentPanel.dispose();
233 | }
234 | currentTitle = title;
235 |
236 | if (!currentPanel) {
237 | currentPanel = window.createWebviewPanel("100doc", title, ViewColumn.One, { enableScripts: true });
238 | currentPanel.onDidDispose(() => {
239 | currentPanel = undefined;
240 | });
241 |
242 | currentPanel.webview.onDidReceiveMessage(async (message) => {
243 | switch (message.command) {
244 | case "incrementShare":
245 | updateMilestoneShare(message.value);
246 | checkSharesMilestones();
247 | break;
248 | case "refreshView":
249 | // refresh the milestones
250 | await MilestoneEventManager.getInstance().fetchAllMilestones();
251 | if (currentPanel) {
252 | // dipose the previous one
253 | currentPanel.dispose();
254 | }
255 | commands.executeCommand("DoC.viewMilestones");
256 | break;
257 | case "logInToAccount":
258 | commands.executeCommand("codetime.codeTimeExisting");
259 | break;
260 | }
261 | });
262 | }
263 |
264 | currentPanel.webview.html = generatedHtml;
265 | currentPanel.reveal(ViewColumn.One);
266 |
267 | displayLoginPromptIfNotLoggedIn();
268 | })
269 | );
270 |
271 | cmds.push(
272 | commands.registerCommand("DoC.addLog", () => {
273 | const generatedHtml = getAddLogHtmlString();
274 |
275 | const title = "Add Daily Progress Log";
276 | if (currentPanel && title !== currentTitle) {
277 | // dipose the previous one
278 | currentPanel.dispose();
279 | }
280 | currentTitle = title;
281 |
282 | if (!currentPanel) {
283 | currentPanel = window.createWebviewPanel("100doc", title, ViewColumn.One, { enableScripts: true });
284 | currentPanel.onDidDispose(() => {
285 | currentPanel = undefined;
286 | });
287 | // handle submit or cancel
288 | let log;
289 | currentPanel.webview.onDidReceiveMessage(async (message) => {
290 | switch (message.command) {
291 | // no need to add a cancel, just show the logs as a default
292 | case "log":
293 | if (isLoggedIn()) {
294 | log = message.value;
295 | // this posts the log create/update to the server as well
296 | await addLogToJson(log.title, log.description, log.hours, log.keystrokes, log.lines, log.links);
297 | checkLanguageMilestonesAchieved();
298 | checkDaysMilestones();
299 | await syncLogs();
300 | } else {
301 | displayLoginPromptIfNotLoggedIn();
302 | }
303 | break;
304 | case "logInToAccount":
305 | commands.executeCommand("codetime.codeTimeExisting");
306 | break;
307 | }
308 | commands.executeCommand("DoC.viewLogs");
309 | });
310 | }
311 |
312 | currentPanel.webview.html = generatedHtml;
313 | currentPanel.reveal(ViewColumn.One);
314 | })
315 | );
316 |
317 | cmds.push(
318 | commands.registerCommand("DoC.showInfoMessage", (tile: string, message: string, isModal: boolean, commandCallback: string) => {
319 | window
320 | .showInformationMessage(
321 | message,
322 | {
323 | modal: isModal,
324 | },
325 | tile
326 | )
327 | .then((selection) => {
328 | if (commandCallback && selection === tile) {
329 | commands.executeCommand(commandCallback);
330 | }
331 | });
332 | })
333 | );
334 |
335 | cmds.push(
336 | commands.registerCommand("DoC.restartChallengeRound", () => {
337 | if (!isLoggedIn()) {
338 | displayLoginPromptIfNotLoggedIn();
339 | return;
340 | }
341 | window
342 | .showInformationMessage(
343 | "Are you sure you want to restart the challenge? Once your challenge is restarted, you will not see your stats from your previous challenge.",
344 | {
345 | modal: true,
346 | },
347 | YES_LABEL
348 | )
349 | .then((selection) => {
350 | if (selection === YES_LABEL) {
351 | // set the new challenge round and create a new log based on the new round val
352 | restartChallenge();
353 | }
354 | });
355 | })
356 | );
357 |
358 | return Disposable.from(...cmds);
359 | }
360 |
--------------------------------------------------------------------------------
/src/utils/Constants.ts:
--------------------------------------------------------------------------------
1 | export const OK_LABEL = "Ok";
2 | export const YES_LABEL = "Yes";
3 | export const _100_DAYS_OF_CODE_EXT_ID = "softwaredotcom.swdc-100-days-of-code";
4 | export const _100_DAYS_OF_CODE_PLUGIN_ID = 18;
5 |
6 | // API ENDPOINT
7 | export const api_endpoint = "https://api.software.com";
8 |
9 | // DASHBOARD URL
10 | export const launch_url = "https://app.software.com";
11 |
12 | export const monthNames = [
13 | "January",
14 | "February",
15 | "March",
16 | "April",
17 | "May",
18 | "June",
19 | "July",
20 | "August",
21 | "September",
22 | "October",
23 | "November",
24 | "December"
25 | ];
26 |
27 | export const HOURS_THRESHOLD = 0.5;
28 |
29 | export const NO_TITLE_LABEL = "No Title";
30 |
--------------------------------------------------------------------------------
/src/utils/DashboardUtil.ts:
--------------------------------------------------------------------------------
1 | import path = require("path");
2 | import fs = require("fs");
3 | import { getDaysLevel, getHoursLevel, getLongStreakLevel, getLinesAddedLevel, getCurrentChallengeRound } from "./SummaryUtil";
4 | import { Summary } from "../models/Summary";
5 | import { getLastSevenLoggedDays, getAllCodetimeHours, getLogDateRange } from "./LogsUtil";
6 | import { getMilestoneById } from "./MilestonesUtil";
7 | import { monthNames } from "./Constants";
8 | import { window } from "vscode";
9 | import { fetchSummaryJsonFileData } from "../managers/FileManager";
10 | import { formatNumber, isLoggedIn } from "./Util";
11 |
12 | function getDashboardTemplate(): string {
13 | return path.join(__dirname, "/assets/templates/dashboard.template.html");
14 | }
15 |
16 | export function getCertificateHtmlString(name: string): string {
17 | return [
18 | ``,
19 | `\t`,
20 | `\t\t`,
21 | `\t\t\t
`,
27 | `\t\t\t
`,
40 | `\t\t\t\t${name}`,
41 | `\t\t\t
`,
42 | `\t\t
`,
43 | `\t`,
44 | ``
45 | ].join("\n");
46 | }
47 |
48 | function getDaysLevelTooltipText(level: number): string {
49 | switch (level) {
50 | case 0:
51 | return `Complete 1 day to reach Level 1.`;
52 | case 1:
53 | return `Complete 10 days to reach Level 2.`;
54 | case 2:
55 | return `Complete 50 days to reach Level 3.`;
56 | case 3:
57 | return `Complete 75 days to reach Level 4.`;
58 | case 4:
59 | return `Complete 100 days to reach Level 5.`;
60 | case 5:
61 | return `Complete 110 days to reach Level ∞ .`;
62 | case 6:
63 | return `Congratulations, you're at Level ∞ !`;
64 | default:
65 | return "";
66 | }
67 | }
68 |
69 | function getHoursLevelTooltipText(level: number): string {
70 | switch (level) {
71 | case 0:
72 | return `Code 1 hour to reach Level 1.`;
73 | case 1:
74 | return `Code 30 hours to reach Level 2.`;
75 | case 2:
76 | return `Code 60 hours to reach Level 3.`;
77 | case 3:
78 | return `Code 90 hours to reach Level 4.`;
79 | case 4:
80 | return `Code 120 hours to reach Level 5.`;
81 | case 5:
82 | return `Code 200 hours to reach Level ∞ .`;
83 | case 6:
84 | return `Congratulations, you're at Level ∞ !`;
85 | default:
86 | return "";
87 | }
88 | }
89 |
90 | function getStreaksLevelTooltipText(level: number): string {
91 | switch (level) {
92 | case 0:
93 | return `Complete a 2-day streak to reach Level 1.`;
94 | case 1:
95 | return `Complete a 7-day streak to reach Level 2.`;
96 | case 2:
97 | return `Complete a 14-day streak to reach Level 3.`;
98 | case 3:
99 | return `Complete a 30-day streak to reach Level 4.`;
100 | case 4:
101 | return `Complete a 60-day streak to reach Level 5.`;
102 | case 5:
103 | return `Complete a 100-day streak to reach Level ∞ .`;
104 | case 6:
105 | return `Congratulations, you're at Level ∞ !`;
106 | default:
107 | return "";
108 | }
109 | }
110 |
111 | function getLinesAddedLevelTooltipText(level: number): string {
112 | switch (level) {
113 | case 0:
114 | return `Write 1 line of code to reach Level 1.`;
115 | case 1:
116 | return `Write 16 lines of code to reach Level 2.`;
117 | case 2:
118 | return `Write 50 lines of code to reach Level 3.`;
119 | case 3:
120 | return `Write 100 lines of code to reach Level 4.`;
121 | case 4:
122 | return `Write 1,000 lines of code to reach Level 5.`;
123 | case 5:
124 | return `Write 10,000 lines of code to reach Level ∞ .`;
125 | case 6:
126 | return `Congratulations, you're at Level ∞ !`;
127 | default:
128 | return "";
129 | }
130 | }
131 |
132 | function generateShareUrl(days: number, hours: number, streaks: number, linesAdded: number, avgHours: number): string {
133 | const hashtagURI = "%23";
134 | const shareText = [
135 | `\n\nDays coded: ${days}`,
136 | `Longest streak: ${streaks} days`,
137 | `Total hours coded: ${hours} hrs`,
138 | `Total lines added: ${linesAdded}`,
139 | `Avg hours/day: ${avgHours} hrs\n\n`
140 | ].join("\n");
141 | const shareURI = encodeURI(shareText);
142 | return `https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.software.com%2F100-days-of-code&text=My%20${hashtagURI}100DaysOfCode%20progress:${shareURI}via%20@software_hq's%20${hashtagURI}vscode%20extension`;
143 | }
144 |
145 | function getStyleColorsBasedOnMode(): any {
146 | const tempWindow: any = window;
147 |
148 | let cardTextColor = "#FFFFFF";
149 | let cardBackgroundColor = "rgba(255,255,255,0.05)";
150 | let datagramXMinColor = "rgba(170,170,170,1)";
151 | let datagramBackground = "rgba(0,0,0,0);";
152 | let cardToolTipColor = "rgba(109,109,109,0.9)";
153 | if (tempWindow.activeColorTheme.kind === 1) {
154 | cardTextColor = "#444444";
155 | cardBackgroundColor = "rgba(0,0,0,0.10)";
156 | datagramXMinColor = "#444444";
157 | datagramBackground = "rgba(0,0,0,0.10);";
158 | cardToolTipColor = "rgba(165,165,165,0.9)";
159 | }
160 | return { cardTextColor, cardBackgroundColor, datagramXMinColor, datagramBackground, cardToolTipColor };
161 | }
162 |
163 | function getDatagramHtmlComponents(): any {
164 | // Datagram
165 | const codeTimeHours: Array = getAllCodetimeHours();
166 | let max = 0;
167 | let mid = 0;
168 | const min = 0;
169 | let barsHtml = "";
170 | let dateJustifyContent = "space-between";
171 | let xAxisDates = "";
172 | if (codeTimeHours.length > 0) {
173 | max = Math.max(...codeTimeHours);
174 | max = parseFloat(max.toFixed(1));
175 | mid = (max - min) / 2;
176 | mid = parseFloat(mid.toFixed(1));
177 | for (let i = 0; i < codeTimeHours.length; i++) {
178 | let size = (codeTimeHours[i] * 250) / max;
179 | let transform = 250 - size;
180 | barsHtml += `\t\t\t\t
\n`;
181 | }
182 | if (codeTimeHours.length < 3) {
183 | dateJustifyContent = "space-around";
184 | }
185 | let datesFromLogs = getLogDateRange();
186 | if (codeTimeHours.length < 10) {
187 | if (codeTimeHours.length > 1) {
188 | const dateObOne = new Date(datesFromLogs[0]);
189 | const dayOne = dateObOne.getDate();
190 | const monthOne = dateObOne.getMonth() + 1;
191 | const dateObTwo = new Date(datesFromLogs[1]);
192 | const dayTwo = dateObTwo.getDate();
193 | const monthTwo = dateObTwo.getMonth() + 1;
194 | xAxisDates = [
195 | `\t\t\t\t${monthOne}/${dayOne}
`,
196 | `\t\t\t\t${monthTwo}/${dayTwo}
`
197 | ].join("\n");
198 | } else {
199 | const dateObOne = new Date(datesFromLogs[0]);
200 | const dayOne = dateObOne.getDate();
201 | const monthOne = dateObOne.getMonth() + 1;
202 | xAxisDates = `\t\t\t\t${monthOne}/${dayOne}
`;
203 | }
204 | } else {
205 | const midDate = Math.ceil((datesFromLogs[0] + datesFromLogs[1]) / 2);
206 | const dateObOne = new Date(datesFromLogs[0]);
207 | const dayOne = dateObOne.getDate();
208 | const monthOne = dateObOne.getMonth() + 1;
209 | const dateObTwo = new Date(midDate);
210 | const dayTwo = dateObTwo.getDate();
211 | const monthTwo = dateObTwo.getMonth() + 1;
212 | const dateObThree = new Date(datesFromLogs[1]);
213 | const dayThree = dateObThree.getDate();
214 | const monthThree = dateObThree.getMonth() + 1;
215 | xAxisDates = [
216 | `\t\t\t\t${monthOne}/${dayOne}
`,
217 | `\t\t\t\t${monthTwo}/${dayTwo}
`,
218 | `\t\t\t\t${monthThree}/${dayThree}
`
219 | ].join("\n");
220 | }
221 | }
222 | // no days
223 | if (barsHtml === "" || max === 0) {
224 | barsHtml = `No days logged yet.
`;
225 | max = 1;
226 | mid = 0.5;
227 | }
228 |
229 | return { barsHtml, xAxisDates, min, max, mid, dateJustifyContent };
230 | }
231 |
232 | function getLogsHtml(): string {
233 | // Logs
234 | const logs = getLastSevenLoggedDays();
235 | let logsHtml = "";
236 |
237 | const d = new Date();
238 | if (logs.length === 0) {
239 | logsHtml = `Complete your first log entry at the end of the day.
`;
240 | } else {
241 | for (let i = 0; i < logs.length; i++) {
242 | logsHtml += `\t\t\t\n\t\t\t\t${logs[i].day_number} \n`;
243 | const dateOb = new Date(logs[i].date);
244 | const day = dateOb.getDate();
245 | const month = monthNames[dateOb.getMonth()];
246 | const year = dateOb.getFullYear();
247 | logsHtml += `\t\t\t\t${day} ${month} ${year} \n`;
248 | logsHtml += `\t\t\t\t${logs[i].title} \n\t\t\t
`;
249 | }
250 | }
251 | return logsHtml;
252 | }
253 |
254 | function getMilestonesHtml(recent_milestones: Array): string {
255 | // Milestones
256 | let milestoneHtml = "";
257 | if (recent_milestones.length > 0) {
258 | let count = 3;
259 | for (let i = 0; i < recent_milestones.length; i++) {
260 | const milestoneId = recent_milestones[i];
261 | const milestone = getMilestoneById(milestoneId);
262 | if (milestone) {
263 | milestoneHtml += [
264 | `\t\t\t\t`,
265 | `\t\t\t\t\t
`,
267 | `\t\t\t\t\t
`,
268 | `\t\t\t\t\t\t
${milestone.title}
`,
269 | `\t\t\t\t\t\t
${milestone.description}
`,
270 | `\t\t\t\t\t
`,
271 | `\t\t\t\t
`
272 | ].join("\n");
273 | }
274 | count -= 1;
275 | if (count === 0) {
276 | break;
277 | }
278 | }
279 | } else {
280 | milestoneHtml = `Check back later for achieved milestones.
`;
281 | }
282 | return milestoneHtml;
283 | }
284 |
285 | export function getUpdatedDashboardHtmlString(): string {
286 | const summary: Summary = fetchSummaryJsonFileData();
287 |
288 | // Metrics
289 | let hours = summary.hours + summary.currentHours;
290 | let days = summary.days;
291 | let streaks = summary.longest_streak;
292 | const linesAdded = summary.lines_added + summary.currentLines;
293 | let avgHours = days > 0 ? parseFloat((hours / days).toFixed(2)) : 0;
294 |
295 | // view certificate if coded over 100 days
296 | let certificateVisibility = "hidden";
297 | if (days >= 100) {
298 | certificateVisibility = "visible";
299 | }
300 |
301 | const { daysLevel, daysProgressPercentage } = getDaysLevel(days);
302 | const { hoursLevel, hoursProgressPercentage } = getHoursLevel(hours);
303 | const { streaksLevel, streaksProgressPercentage } = getLongStreakLevel(streaks);
304 | const { linesAddedLevel, linesAddedProgressPercentage } = getLinesAddedLevel(linesAdded);
305 |
306 | const daysLevelTooltip = getDaysLevelTooltipText(daysLevel);
307 | const hoursLevelTooltip = getHoursLevelTooltipText(hoursLevel);
308 | const streaksLevelTooltip = getStreaksLevelTooltipText(streaksLevel);
309 | const linesAddedLevelTooltip = getLinesAddedLevelTooltipText(linesAddedLevel);
310 | const avgHoursLevelTooltip =
311 | avgHours >= 1 ? "Great job! You're on track to coding one hour every day." : "Try to code an hour each day.";
312 |
313 | const twitterShareUrl = generateShareUrl(days, hours, streaks, linesAdded, avgHours);
314 |
315 | const {
316 | cardTextColor,
317 | cardBackgroundColor,
318 | datagramXMinColor,
319 | datagramBackground,
320 | cardToolTipColor
321 | } = getStyleColorsBasedOnMode();
322 |
323 | const { barsHtml, xAxisDates, min, max, mid, dateJustifyContent } = getDatagramHtmlComponents();
324 |
325 | const logsHtml = getLogsHtml();
326 |
327 | const milestoneHtml = getMilestonesHtml(summary.recent_milestones);
328 |
329 | const dayProgressPx = Math.round(daysProgressPercentage * 1.2);
330 | const hoursProgressPx = Math.round(hoursProgressPercentage * 1.2);
331 | const streakProgressPx = Math.round(streaksProgressPercentage * 1.2);
332 | const lineAddedProgressPx = Math.round(linesAddedProgressPercentage * 1.2);
333 | const avgHoursProgressPx = avgHours >= 1 ? 120 : Math.round(avgHours * 120);
334 |
335 | const dayProgressColor = daysLevel === 6 ? "#FD9808" : "#00b4ee";
336 | const hoursProgressColor = hoursLevel === 6 ? "#FD9808" : "#00b4ee";
337 | const streakProgressColor = streaksLevel === 6 ? "#FD9808" : "#00b4ee";
338 | const lineAddedProgressColor = linesAddedLevel === 6 ? "#FD9808" : "#00b4ee";
339 | const avgHoursProgressColor = avgHours >= 1 ? "#FD9808" : "#00b4ee";
340 |
341 | let logInVisibility = "hidden";
342 | let logInMessageDisplay = "none";
343 | if (!isLoggedIn()) {
344 | logInVisibility = "visible";
345 | logInMessageDisplay = "";
346 | }
347 |
348 | const templateVars = {
349 | currentChallengeRound: getCurrentChallengeRound(),
350 | hours: formatNumber(hours),
351 | days,
352 | streaks,
353 | linesAdded: formatNumber(linesAdded),
354 | avgHours,
355 | daysLevelTooltip,
356 | hoursLevelTooltip,
357 | streaksLevelTooltip,
358 | linesAddedLevelTooltip,
359 | avgHoursLevelTooltip,
360 | twitterShareUrl,
361 | cardTextColor,
362 | cardBackgroundColor,
363 | datagramXMinColor,
364 | datagramBackground,
365 | cardToolTipColor,
366 | barsHtml,
367 | xAxisDates,
368 | min,
369 | max,
370 | mid,
371 | dateJustifyContent,
372 | logsHtml,
373 | milestoneHtml,
374 | certificateVisibility,
375 | dayProgressPx,
376 | hoursProgressPx,
377 | streakProgressPx,
378 | lineAddedProgressPx,
379 | avgHoursProgressPx,
380 | dayProgressColor,
381 | hoursProgressColor,
382 | streakProgressColor,
383 | lineAddedProgressColor,
384 | avgHoursProgressColor,
385 | logInVisibility,
386 | logInMessageDisplay
387 | };
388 |
389 | const templateString = fs.readFileSync(getDashboardTemplate()).toString();
390 | const fillTemplate = function (templateString: string, templateVars: any) {
391 | return new Function("return `" + templateString + "`;").call(templateVars);
392 | };
393 |
394 | const dashboardHtmlContent = fillTemplate(templateString, templateVars);
395 | return dashboardHtmlContent;
396 | }
397 |
--------------------------------------------------------------------------------
/src/utils/LanguageUtil.ts:
--------------------------------------------------------------------------------
1 | import { compareDates } from "./Util";
2 | import { getFile, getFileDataAsJson } from "../managers/FileManager";
3 |
4 | function getFileSummaryJson() {
5 | return getFile("fileChangeSummary.json");
6 | }
7 |
8 | export function getLanguages() {
9 | const fileSummary = getFileDataAsJson(getFileSummaryJson());
10 |
11 | let languages: Array = [];
12 | if (fileSummary) {
13 | const dateNow = new Date();
14 |
15 | for (const key in fileSummary) {
16 | const endTime = fileSummary[key]["end"] * 1000; // seconds to milliseconds
17 | // checks if edited today
18 | if (compareDates(dateNow, new Date(endTime))) {
19 | const language = fileSummary[key]["syntax"];
20 | languages.push(language);
21 | }
22 | }
23 | }
24 |
25 | // for no duplicates, we convert array into set and back into array
26 | languages = Array.from(new Set(languages));
27 |
28 | return languages;
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/LogsTemplateUtil.ts:
--------------------------------------------------------------------------------
1 | import { window } from "vscode";
2 | import { Summary } from "../models/Summary";
3 | import { getMilestoneById } from "./MilestonesUtil";
4 | import { Log } from "../models/Log";
5 | import { getAllDescendingOrderLogObjects, getMostRecentLogObject } from "./LogsUtil";
6 | import path = require("path");
7 | import fs = require("fs");
8 | import { monthNames, NO_TITLE_LABEL } from "./Constants";
9 | import { fetchSummaryJsonFileData } from "../managers/FileManager";
10 | import { getInputFormStyles, isLoggedIn } from "./Util";
11 |
12 | function getLogsTemplate() {
13 | return path.join(__dirname, "/assets/templates/logs.template.html");
14 | }
15 |
16 | function getStyleColorsBasedOnMode(): any {
17 | const tempWindow: any = window;
18 |
19 | const { cardTextColor, cardBackgroundColor } = getInputFormStyles();
20 | const editLogCardColor = cardTextColor;
21 |
22 | let cardMetricBarSidesColor = "rgba(255,255,255,0.20)";
23 | let editButtonColor = "rgba(255,255,255,0.10)";
24 | let cardToolTipColor = "rgba(109, 109, 109, .9)";
25 | let editPath = "https://100-days-of-code.s3-us-west-1.amazonaws.com/edit.svg";
26 | let dropDownPath = "https://100-days-of-code.s3-us-west-1.amazonaws.com/Logs/dropDown.svg";
27 | let deletePath = "https://100-days-of-code.s3-us-west-1.amazonaws.com/delete.svg";
28 | let lightGrayColor = "#919eab";
29 | if (tempWindow.activeColorTheme.kind === 1) {
30 | cardMetricBarSidesColor = "rgba(0,0,0,0.20)";
31 | editButtonColor = "rgba(0,0,0,0.10)";
32 | cardToolTipColor = "rgba(165, 165, 165, .9)";
33 | lightGrayColor = "#596673";
34 | }
35 | return {
36 | cardTextColor,
37 | cardBackgroundColor,
38 | cardMetricBarSidesColor,
39 | cardToolTipColor,
40 | editPath,
41 | deletePath,
42 | dropDownPath,
43 | editLogCardColor,
44 | editButtonColor,
45 | lightGrayColor
46 | };
47 | }
48 |
49 | function generateShareUrl(
50 | day_number: number,
51 | title: string,
52 | hours: number,
53 | keystrokes: number,
54 | lines_added: number
55 | ): string {
56 | // Share link
57 | let dayURI = encodeURI(`Day ${day_number}/100 of`);
58 | let shareText = [
59 | `\n\n${title}`,
60 | `\nHours coded: ${hours}`,
61 | `Lines of code: ${lines_added}`,
62 | `Keystrokes: ${keystrokes}\n\n`
63 | ].join("\n");
64 | const shareURI = encodeURI(shareText);
65 | const hashtagURI = "%23";
66 | return `https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.software.com%2F100-days-of-code&text=${dayURI}%20${hashtagURI}100DaysOfCode${shareURI}via%20@software_hq's%20${hashtagURI}vscode%20extension`;
67 | }
68 |
69 | function getFormattedDate(timestamp: number): string {
70 | const date = new Date(timestamp);
71 | const dayOfMonth = date.getDate();
72 | const month = monthNames[date.getMonth()];
73 | const year = date.getFullYear();
74 | return month + " " + dayOfMonth + ", " + year;
75 | }
76 |
77 | function getLogsCardSummaryVariables(dayHours: number, dayKeystrokes: number, dayLinesAdded: number) {
78 | const summary: Summary = fetchSummaryJsonFileData();
79 | const hours = summary.hours + summary.currentHours;
80 | const keystrokes = summary.keystrokes + summary.currentKeystrokes;
81 | const lines = summary.lines_added + summary.currentLines;
82 | const days = summary.days;
83 | let avgHours = parseFloat((hours / days).toFixed(2));
84 | let avgKeystrokes = parseFloat((keystrokes / days).toFixed(2));
85 | let avgLines = parseFloat((lines / days).toFixed(2));
86 |
87 | let percentHours = (dayHours / avgHours) * 100;
88 | percentHours = Math.round(percentHours * 100) / 100;
89 | if (!avgHours || avgHours === 0) {
90 | percentHours = 100;
91 | avgHours = 0;
92 | }
93 | let percentKeystrokes = (dayKeystrokes / avgKeystrokes) * 100;
94 | percentKeystrokes = Math.round(percentKeystrokes * 100) / 100;
95 | if (!avgKeystrokes || avgKeystrokes === 0) {
96 | percentKeystrokes = 100;
97 | avgKeystrokes = 0;
98 | }
99 | let percentLines = (dayLinesAdded / avgLines) * 100;
100 | percentLines = Math.round(percentLines * 100) / 100;
101 | if (!avgLines || avgLines === 0) {
102 | percentLines = 100;
103 | avgLines = 0;
104 | }
105 |
106 | let barPxHours = Math.round(percentHours * 1.3);
107 | let barColorHours = "00b4ee";
108 | if (barPxHours >= 130) {
109 | barPxHours = 130;
110 | barColorHours = "FD9808";
111 | } else if (barPxHours < 20 && barPxHours > 0) {
112 | barPxHours = 20;
113 | }
114 | let barPxKeystrokes = Math.round(percentKeystrokes * 1.3);
115 | let barColorKeystrokes = "00b4ee";
116 | if (barPxKeystrokes >= 130) {
117 | barPxKeystrokes = 130;
118 | barColorKeystrokes = "FD9808";
119 | } else if (barPxKeystrokes < 20 && barPxKeystrokes > 0) {
120 | barPxKeystrokes = 20;
121 | }
122 | let barPxLines = Math.round(percentLines * 1.3);
123 | let barColorLines = "00b4ee";
124 | if (barPxLines >= 130) {
125 | barPxLines = 130;
126 | barColorLines = "FD9808";
127 | } else if (barPxLines < 20 && barPxLines > 0) {
128 | barPxLines = 20;
129 | }
130 |
131 | return {
132 | avgHours,
133 | percentHours,
134 | barPxHours,
135 | barColorHours,
136 | avgKeystrokes,
137 | percentKeystrokes,
138 | barPxKeystrokes,
139 | barColorKeystrokes,
140 | avgLines,
141 | percentLines,
142 | barPxLines,
143 | barColorLines
144 | };
145 | }
146 |
147 | function getLinksText(links: Array): string {
148 | let linksText = "";
149 | for (let i = 0; i < links.length; i++) {
150 | linksText += [
151 | `\t\t\t\t\t\t`,
152 | `\t\t\t\t\t\t\t${links[i]}`,
153 | `\t\t\t\t\t\t \n`
154 | ].join("\n");
155 | }
156 | return linksText;
157 | }
158 |
159 | function getMilestonesText(milestones: Array): string {
160 | let milestonesText = "";
161 | const milestoneNum = milestones.length;
162 | for (let milestoneIndex = 0; milestoneIndex < 9; milestoneIndex++) {
163 | if (milestoneIndex % 3 === 0) {
164 | milestonesText += `\t\t\t\t\t\n`;
165 | }
166 |
167 | if (milestoneIndex < milestoneNum) {
168 | let milestoneId = milestones[milestoneIndex];
169 | let milestone = getMilestoneById(milestoneId);
170 | if (milestone) {
171 | milestonesText += [
172 | `\t\t\t\t\t\t
`,
173 | `\t\t\t\t\t\t\t
`,
174 | `\t\t\t\t\t\t\t\t${milestone.title}
`,
175 | `\t\t\t\t\t\t\t\t${milestone.description}
`,
176 | `\t\t\t\t\t\t\t `,
177 | `\t\t\t\t\t\t\t
`,
178 | `\t\t\t\t\t\t
\n`
179 | ].join("\n");
180 | }
181 | } else {
182 | milestonesText += [`\t\t\t\t\t\t
`, `\t\t\t\t\t\t
\n`].join("\n");
183 | }
184 |
185 | if (milestoneIndex % 3 === 2) {
186 | milestonesText += `\t\t\t\t\t
\n`;
187 | }
188 | }
189 | return milestonesText;
190 | }
191 |
192 | function getLogCard(
193 | day: Log,
194 | formattedDate: string,
195 | twitterShareUrl: string,
196 | shareIconLink: string,
197 | editPath: string,
198 | deletePath: string,
199 | dropDownPath: string
200 | ): string {
201 | const {
202 | avgHours,
203 | percentHours,
204 | barPxHours,
205 | barColorHours,
206 | avgKeystrokes,
207 | percentKeystrokes,
208 | barPxKeystrokes,
209 | barColorKeystrokes,
210 | avgLines,
211 | percentLines,
212 | barPxLines,
213 | barColorLines
214 | } = getLogsCardSummaryVariables(
215 | day.codetime_metrics.hours,
216 | day.codetime_metrics.keystrokes,
217 | day.codetime_metrics.lines_added
218 | );
219 | const { lightGrayColor } = getStyleColorsBasedOnMode();
220 | const linksText = getLinksText(day.links);
221 | const milestonesText = getMilestonesText(day.milestones);
222 | return [
223 | `\t`,
224 | `\t\t`,
238 | `\t\t
`,
239 | `\t\t\t
`,
240 | `\t\t\t\t
`,
241 | `\t\t\t\t\t
${day.description}
`,
242 | `\t\t\t\t\t
`,
243 | `\t\t\t\t
`,
244 | `\t\t\t\t
`,
245 | `\t\t\t\t\t
\n`,
246 | `${linksText}`,
247 | `\t\t\t\t\t
`,
248 | `\t\t\t\t
`,
249 | `\t\t\t
`,
250 | `\t\t\t
`,
251 | `\t\t\t\t
Coding Metrics
`,
252 | `\t\t\t\t
`,
253 | `\t\t\t\t
`,
254 | `\t\t\t\t\t
`,
255 | `\t\t\t\t\t\t
Code Time
`,
256 | `\t\t\t\t\t\t
${day.codetime_metrics.hours}
`,
257 | `\t\t\t\t\t\t
${percentHours}% of average
`,
258 | `\t\t\t\t\t\t
Average: ${avgHours} Hours
`,
259 | `\t\t\t\t\t\t
`,
260 | `\t\t\t\t\t\t\t
`,
261 | `\t\t\t\t\t\t\t
`,
262 | `\t\t\t\t\t\t
`,
263 | `\t\t\t\t\t
`,
264 | `\t\t\t\t\t
`,
265 | `\t\t\t\t\t\t
Keystrokes
`,
266 | `\t\t\t\t\t\t
${day.codetime_metrics.keystrokes}
`,
267 | `\t\t\t\t\t\t
${percentKeystrokes}% of average
`,
268 | `\t\t\t\t\t\t
Average: ${avgKeystrokes}
`,
269 | `\t\t\t\t\t\t
`,
270 | `\t\t\t\t\t\t\t
`,
271 | `\t\t\t\t\t\t\t
`,
272 | `\t\t\t\t\t\t\t
`,
273 | `\t\t\t\t\t\t\t
`,
274 | `\t\t\t\t\t\t
`,
275 | `\t\t\t\t\t
`,
276 | `\t\t\t\t\t
`,
277 | `\t\t\t\t\t\t
Lines Added
`,
278 | `\t\t\t\t\t\t
${day.codetime_metrics.lines_added}
`,
279 | `\t\t\t\t\t\t
${percentLines}% of average
`,
280 | `\t\t\t\t\t\t
Average: ${avgLines}
`,
281 | `\t\t\t\t\t\t
`,
282 | `\t\t\t\t\t\t\t
`,
283 | `\t\t\t\t\t\t\t
`,
284 | `\t\t\t\t\t\t\t
`,
285 | `\t\t\t\t\t\t\t
`,
286 | `\t\t\t\t\t\t
`,
287 | `\t\t\t\t\t
`,
288 | `\t\t\t\t
`,
289 | `\t\t\t
`,
290 | `\t\t\t
`,
291 | `\t\t\t\t
Milestones
`,
292 | `\t\t\t\t
`,
293 | `\t\t\t\t
\n`,
294 | `${milestonesText}`,
295 | `\t\t\t\t\t
`,
296 | `\t\t\t\t
`,
297 | `\t\t\t
`,
298 | `\t\t
`,
299 | `\t\n`
300 | ].join("\n");
301 | }
302 |
303 | export function getUpdatedLogsHtml(): string {
304 | let logs: Array = getAllDescendingOrderLogObjects();
305 |
306 | // if in light mode
307 | const {
308 | cardTextColor,
309 | cardBackgroundColor,
310 | cardMetricBarSidesColor,
311 | cardToolTipColor,
312 | editPath,
313 | deletePath,
314 | dropDownPath,
315 | editLogCardColor,
316 | editButtonColor
317 | } = getStyleColorsBasedOnMode();
318 |
319 | // CSS
320 | let logsHtml = "";
321 | let addLogVisibility = "hidden";
322 | let logInVisibility = "hidden";
323 | let logInMessageDisplay = "none";
324 | if (!isLoggedIn()) {
325 | logInVisibility = "visible";
326 | logInMessageDisplay = "";
327 | }
328 |
329 | const mostRecentLog = getMostRecentLogObject();
330 | if (mostRecentLog && (mostRecentLog.title === NO_TITLE_LABEL || !mostRecentLog.description)) {
331 | addLogVisibility = "visible";
332 | }
333 |
334 | if (logs && logs.length) {
335 |
336 | for (let log of logs) {
337 | const twitterShareUrl = generateShareUrl(
338 | log.day_number,
339 | log.title,
340 | log.codetime_metrics.hours,
341 | log.codetime_metrics.keystrokes,
342 | log.codetime_metrics.lines_added
343 | );
344 |
345 | const formattedDate = getFormattedDate(log.date);
346 |
347 | const shareIconLink = "https://100-days-of-code.s3-us-west-1.amazonaws.com/Milestones/share.svg";
348 |
349 | logsHtml += getLogCard(log, formattedDate, twitterShareUrl, shareIconLink, editPath, deletePath, dropDownPath);
350 | }
351 | } else {
352 | addLogVisibility = "visible";
353 | logsHtml = `\t\tLog Daily Progress to see it here! `;
354 | }
355 |
356 | const templateVars = {
357 | logsHtml,
358 | cardTextColor,
359 | cardBackgroundColor,
360 | cardMetricBarSidesColor,
361 | cardToolTipColor,
362 | editPath,
363 | dropDownPath,
364 | editLogCardColor,
365 | editButtonColor,
366 | addLogVisibility,
367 | logInVisibility,
368 | logInMessageDisplay
369 | };
370 |
371 | const templateString = fs.readFileSync(getLogsTemplate()).toString();
372 | const fillTemplate = function (templateString: string, templateVars: any) {
373 | return new Function("return `" + templateString + "`;").call(templateVars);
374 | };
375 |
376 | const logsHtmlContent = fillTemplate(templateString, templateVars);
377 | return logsHtmlContent;
378 | }
379 |
--------------------------------------------------------------------------------
/src/utils/LogsUtil.ts:
--------------------------------------------------------------------------------
1 | import { compareDates, isLoggedIn } from "./Util";
2 | import fs = require("fs");
3 | import { CodetimeMetrics } from "../models/CodetimeMetrics";
4 | import { Log } from "../models/Log";
5 | import {
6 | incrementSummaryShare,
7 | updateSummaryJson,
8 | getSummaryTotalHours,
9 | setSummaryCurrentHours,
10 | setSummaryTotalHours,
11 | getCurrentChallengeRound
12 | } from "./SummaryUtil";
13 | import { getFileDataAsJson, getFile } from "../managers/FileManager";
14 | import { commands, window } from "vscode";
15 | import { getAllMilestones } from "./MilestonesUtil";
16 | import { NO_TITLE_LABEL } from "./Constants";
17 |
18 | const moment = require("moment-timezone");
19 |
20 | let currently_deleting_log_date: number = -1;
21 |
22 | export function getLogsFilePath(): string {
23 | return getFile("logs.json");
24 | }
25 |
26 | export function deleteLogsJson() {
27 | const filepath = getLogsFilePath();
28 | const fileExists = fs.existsSync(filepath);
29 | if (fileExists) {
30 | fs.unlinkSync(filepath);
31 | }
32 | }
33 |
34 | export function getAllDescendingOrderLogObjects(): Array {
35 | const filepath = getLogsFilePath();
36 | let logs = getFileDataAsJson(filepath);
37 | if (logs && logs.length) {
38 | // sort by unix_date in descending order
39 | logs = logs.sort(
40 | (a: Log, b: Log) => b.unix_date - a.unix_date
41 | );
42 | }
43 | return logs || [];
44 | }
45 |
46 | export function getLogByUnixDate(unix_date: number): Log {
47 | const logs: Array = getAllDescendingOrderLogObjects();
48 | if (logs && logs.length) {
49 | return logs.find(n => n.unix_date === unix_date);
50 | }
51 | return null;
52 | }
53 |
54 | export function getDayNumberLog(day_number: number): Log {
55 | const logs: Array = getAllDescendingOrderLogObjects();
56 | if (logs && logs.length) {
57 | logs.reverse();
58 | return logs.find(n => n.day_number === day_number);
59 | }
60 | return null;
61 | }
62 |
63 | export function updateDayNumberLog(log: Log) {
64 | const logs: Array = getAllDescendingOrderLogObjects();
65 | if (logs && logs.length) {
66 | logs.reverse();
67 | for (let i = 0; i < logs.length; i++) {
68 | let existingLog = logs[i];
69 | if (existingLog.day_number === log.day_number) {
70 | logs[i] = log;
71 | writeToLogsJson(logs);
72 | break;
73 | }
74 | }
75 | }
76 | }
77 |
78 | export function writeToLogsJson(logs: Array = []) {
79 | const filepath = getLogsFilePath();
80 | try {
81 | fs.writeFileSync(filepath, JSON.stringify(logs, null, 2));
82 | } catch (err) {
83 | console.log(err);
84 | }
85 | }
86 |
87 | export function getLogsSummary(): any {
88 | const logs: Array = getAllDescendingOrderLogObjects();
89 | let totalHours = 0;
90 | let totalLinesAdded = 0;
91 | let totalKeystrokes = 0;
92 | let totalDays = 0;
93 | let longest_streak = 0;
94 | let current_streak = 0;
95 | let currentHours = 0;
96 | let currentKeystrokes = 0;
97 | let currentLines = 0;
98 | let currentDate = 0;
99 |
100 | if (logs.length > 0) {
101 | const hours24 = 86400000;
102 | let previousDate = logs[0].date - hours24;
103 | for (let i = 0; i < logs.length - 1; i++) {
104 | totalHours += logs[i].codetime_metrics.hours;
105 | totalLinesAdded += logs[i].codetime_metrics.lines_added;
106 | totalKeystrokes += logs[i].codetime_metrics.keystrokes;
107 | totalDays++;
108 | if (compareDates(new Date(previousDate + hours24), new Date(logs[i].date))) {
109 | current_streak++;
110 | if (current_streak > longest_streak) {
111 | longest_streak = current_streak;
112 | }
113 | } else {
114 | current_streak = 0;
115 | }
116 |
117 | previousDate = logs[i].date;
118 | }
119 |
120 | const lastLog = logs[logs.length - 1];
121 |
122 | // checks if last log is today
123 | if (compareDates(new Date(lastLog.date), new Date())) {
124 | currentHours = lastLog.codetime_metrics.hours;
125 | currentKeystrokes = lastLog.codetime_metrics.keystrokes;
126 | currentLines = lastLog.codetime_metrics.lines_added;
127 | totalDays++;
128 | } else {
129 | totalHours += lastLog.codetime_metrics.hours;
130 | totalLinesAdded += lastLog.codetime_metrics.lines_added;
131 | totalKeystrokes += lastLog.codetime_metrics.keystrokes;
132 | totalDays++;
133 | }
134 | if (compareDates(new Date(previousDate + hours24), new Date(lastLog.date))) {
135 | current_streak++;
136 | if (current_streak > longest_streak) {
137 | longest_streak = current_streak;
138 | }
139 | } else {
140 | current_streak = 0;
141 | }
142 |
143 | currentDate = lastLog.date;
144 | }
145 |
146 | return {
147 | totalHours,
148 | totalLinesAdded,
149 | totalKeystrokes,
150 | totalDays,
151 | longest_streak,
152 | current_streak,
153 | currentHours,
154 | currentKeystrokes,
155 | currentLines,
156 | currentDate
157 | };
158 | }
159 |
160 | export function getDayNumberFromDate(dateUnix: number): number {
161 | const logs = getAllDescendingOrderLogObjects();
162 | let date = new Date(dateUnix);
163 | for (let log of logs) {
164 | if (compareDates(new Date(log.date), date)) {
165 | return log.day_number;
166 | }
167 | }
168 | return -1;
169 | }
170 |
171 | export function setDailyMilestonesByDayNumber(dayNumber: number, newMilestones: Array) {
172 | let log = getDayNumberLog(dayNumber);
173 | newMilestones = newMilestones.concat(log.milestones);
174 | newMilestones = Array.from(new Set(newMilestones));
175 | log.milestones = newMilestones;
176 | updateDayNumberLog(log);
177 | }
178 |
179 | export async function addLogToJson(
180 | title: string,
181 | description: string,
182 | hours: string,
183 | keystrokes: string,
184 | lines: string,
185 | links: Array
186 | ) {
187 | const numLogs = getLatestLogEntryNumber();
188 |
189 | if (numLogs === 0) {
190 | console.log("Logs json could not be read");
191 | return false;
192 | }
193 |
194 | let codetimeMetrics = new CodetimeMetrics();
195 |
196 | codetimeMetrics.hours = parseFloat(hours);
197 | codetimeMetrics.lines_added = parseInt(lines);
198 | codetimeMetrics.keystrokes = parseInt(keystrokes);
199 |
200 | let log = new Log();
201 | if (title) {
202 | log.title = title;
203 | }
204 | log.description = description;
205 | log.links = links;
206 | log.codetime_metrics = codetimeMetrics;
207 | log.day_number = getDayNumberForNewLog();
208 | log.challenge_round = getCurrentChallengeRound();
209 |
210 | await createLog(log);
211 |
212 | updateSummaryJson();
213 | }
214 |
215 | // Get the last log's day_number. If the date is the
216 | // same use the same day_number. If not, increment it.
217 | export function getDayNumberForNewLog() {
218 | const lastLog = getMostRecentLogObject();
219 | const currentDay = moment().format("YYYY-MM-DD");
220 | const lastLogDay = moment(lastLog.date).format("YYYY-MM-DD");
221 | if (currentDay == lastLogDay) {
222 | return lastLog.day_number;
223 | }
224 | return lastLog.day_number + 1;
225 | }
226 |
227 | export function getLatestLogEntryNumber(): number {
228 | let logs = getAllDescendingOrderLogObjects();
229 | return logs ? logs.length : 0;
230 | }
231 |
232 | export function getMostRecentLogObject(): Log | any {
233 | const logs = getAllDescendingOrderLogObjects();
234 |
235 | if (logs && logs.length > 0) {
236 | // get the most recent one
237 | return logs[0];
238 | }
239 | const log:Log = new Log();
240 | log.day_number = 1;
241 | log.challenge_round = getCurrentChallengeRound();
242 | return log;
243 | }
244 |
245 | export function getLogDateRange(): Array {
246 | const logs = getAllDescendingOrderLogObjects();
247 | let dates = [];
248 | if (logs.length) {
249 | dates.push(logs[0].date);
250 | dates.push(logs[logs.length - 1].date);
251 | }
252 | return dates;
253 | }
254 |
255 | export function getAllCodetimeHours(): Array {
256 | const logs = getAllDescendingOrderLogObjects();
257 | let sendHours: Array = [];
258 | for (let i = 0; i < logs.length; i++) {
259 | if (logs[i].day_number) {
260 | sendHours.push(logs[i].codetime_metrics.hours);
261 | }
262 | }
263 | return sendHours;
264 | }
265 |
266 | export function getLastSevenLoggedDays(): Array {
267 | const logs = getAllDescendingOrderLogObjects();
268 |
269 | let sendLogs: Array = [];
270 | if (logs.length === 0) {
271 | return sendLogs;
272 | }
273 | if (logs[logs.length - 1].title !== NO_TITLE_LABEL) {
274 | sendLogs.push(logs[logs.length - 1]);
275 | }
276 | for (let i = logs.length - 2; i >= 0; i--) {
277 | if (logs[i].day_number) {
278 | sendLogs.push(logs[i]);
279 | if (sendLogs.length === 7) {
280 | return sendLogs;
281 | }
282 | }
283 | }
284 | return sendLogs;
285 | }
286 |
287 | export function checkIfOnStreak(): boolean {
288 | const logs = getAllDescendingOrderLogObjects();
289 | // one day streak
290 | if (logs.length < 2) {
291 | return true;
292 | }
293 | const currDate = new Date(logs[logs.length - 1].date);
294 | const prevDatePlusDay = new Date(logs[logs.length - 2].date + 86400000);
295 | return compareDates(currDate, prevDatePlusDay);
296 | }
297 |
298 | export function updateLogShare(day: number) {
299 | const log = getDayNumberLog(day);
300 | if (log && !log.shared) {
301 | log.shared = true;
302 | incrementSummaryShare();
303 | updateDayNumberLog(log);
304 | }
305 | }
306 |
307 | export async function editLogEntry(
308 | dayNumber: number,
309 | unix_date: number,
310 | title: string,
311 | description: string,
312 | links: Array,
313 | editedHours: number
314 | ) {
315 | let log = getLogByUnixDate(unix_date);
316 | log.title = title;
317 | log.description = description;
318 | log.links = links;
319 | log.unix_date = unix_date;
320 | const currentLoggedHours = log.codetime_metrics.hours;
321 | if (editedHours >= 0 && editedHours <= 12) {
322 | log.codetime_metrics.hours = editedHours;
323 | } else if (editedHours < 0) {
324 | log.codetime_metrics.hours = 0;
325 | } else {
326 | log.codetime_metrics.hours = 12;
327 | }
328 | let summaryTotalHours = getSummaryTotalHours();
329 | if (dayNumber === getAllDescendingOrderLogObjects().length) {
330 | setSummaryCurrentHours(log.codetime_metrics.hours);
331 | } else {
332 | summaryTotalHours -= currentLoggedHours;
333 | summaryTotalHours += log.codetime_metrics.hours;
334 | setSummaryTotalHours(summaryTotalHours);
335 | }
336 | await updateLog(log);
337 | }
338 |
339 | // updates a log locally and on the server
340 | async function updateLog(log: Log) {
341 | // get all log objects
342 | const logs = await getLocalLogsFromFile();
343 |
344 | const index = logs.findIndex(n => n.unix_date === log.unix_date);
345 | // replace
346 | logs[index] = log;
347 | // write back to local
348 | saveLogsToFile(logs);
349 | }
350 |
351 | // creates a new log locally and on the server
352 | export async function createLog(log: Log) {
353 | // get all log objects
354 | const logs = await getLocalLogsFromFile();
355 | // push the new log to the server
356 | const preparedLog:Log = await prepareLogForServerUpdate(log);
357 | // add the new log
358 | const updatedLogs = [...logs, preparedLog];
359 | // write back to the local file
360 | saveLogsToFile(updatedLogs);
361 | }
362 |
363 | export async function deleteLogDay(unix_date: number) {
364 | if (currently_deleting_log_date !== -1) {
365 | window.showInformationMessage("Currently waiting to delete the requested log, please wait.");
366 | return;
367 | }
368 |
369 | currently_deleting_log_date = unix_date;
370 |
371 | window.showInformationMessage("Your log has been successfully deleted.");
372 | // delete the log
373 | let logs: Array = await getLocalLogsFromFile();
374 | // delete the log based on the dayNum
375 | logs = logs.filter((n: Log) => n.unix_date !== unix_date);
376 | saveLogsToFile(logs);
377 | await syncLogs();
378 | commands.executeCommand("DoC.viewLogs");
379 |
380 | currently_deleting_log_date = -1;
381 |
382 | }
383 |
384 | // pulls logs from the server and saves them locally. This will be run periodically.
385 | // logs have a format like [ { day_number: 1, date: ... }, ... ]
386 | export async function syncLogs() {
387 | let serverLogs: Array = getLocalLogsFromFile() || [];
388 |
389 | let createLogForToday = true;
390 | const currentDay = moment().format("YYYY-MM-DD");
391 |
392 | if (serverLogs && serverLogs.length) {
393 | // these come back sorted in ascending order
394 | const formattedLogs = formatLogs(serverLogs);
395 |
396 | // check if we have one for today
397 | const lastLoggedDay = moment(formattedLogs[0].date).format("YYYY-MM-DD");
398 |
399 | // if we don't have a log for today, we'll create an empty one
400 | if (currentDay === lastLoggedDay) {
401 | createLogForToday = false;
402 | }
403 | await addMilestonesToLogs(formattedLogs);
404 | saveLogsToFile(formattedLogs);
405 | }
406 |
407 | if (createLogForToday && isLoggedIn()) {
408 | // create a log for today and add it to the local logs
409 | // await addDailyLog();
410 | const log:Log = new Log();
411 | log.day_number = getDayNumberForNewLog();
412 | log.challenge_round = getCurrentChallengeRound();
413 | await createLog(log);
414 | }
415 | }
416 |
417 | // converts local log to format that server will accept
418 | function prepareLogForServerUpdate(log: Log) {
419 | const offset_seconds = new Date().getTimezoneOffset() * 60;
420 |
421 | let preparedLog = {
422 | ...log,
423 | minutes: log.codetime_metrics.hours * 60,
424 | keystrokes: log.codetime_metrics.keystrokes,
425 | lines_added: log.codetime_metrics.lines_added,
426 | lines_removed: 0,
427 | unix_date: moment(log.date).unix(),
428 | local_date: moment(log.date).unix() - offset_seconds,
429 | offset_minutes: offset_seconds / 60,
430 | timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
431 | challenge_round: log.challenge_round || getCurrentChallengeRound()
432 | };
433 |
434 | return preparedLog;
435 | }
436 |
437 | function saveLogsToFile(logs: Array = []) {
438 | const filePath = getLogFilePath();
439 | try {
440 | fs.writeFileSync(filePath, JSON.stringify(logs, null, 2));
441 | } catch (err) {
442 | console.log(err);
443 | }
444 | }
445 |
446 | function getLocalLogsFromFile(): Array {
447 | const filePath = getLogFilePath();
448 |
449 | let logs: Array = [];
450 | const exists = checkIfLocalFileExists(filePath);
451 | if (exists) {
452 | logs = getFileDataAsJson(filePath);
453 | }
454 | return logs || [];
455 | }
456 |
457 | export function getLogFilePath(): string {
458 | return getFile("logs.json");
459 | }
460 |
461 | function checkIfLocalFileExists(filepath: string): boolean {
462 | if (fs.existsSync(filepath)) {
463 | return true;
464 | } else {
465 | return false;
466 | }
467 | }
468 |
469 | // formats logs from the server into the local log model format before saving locally
470 | // logs have a format like [ { day_number: 1, date: ... }, ... ]
471 | function formatLogs(logs: Array) {
472 | const formattedLogs: Array = [];
473 | logs.forEach((log: any) => {
474 | if (!log.codetime_metrics) {
475 | log.codetime_metrics = new CodetimeMetrics();
476 | }
477 | if (!log.date) {
478 | log.date = log.unix_date * 1000;
479 | }
480 | log.codetime_metrics.hours = log.minutes ? parseFloat((log.minutes / 60).toFixed(2)) : 0;
481 | log.codetime_metrics.keystrokes = log.keystrokes;
482 | log.codetime_metrics.lines_added = log.lines_added;
483 | log.links = log.ref_links || [];
484 | if (!log.challenge_round) {
485 | log.challenge_round = getCurrentChallengeRound();
486 | }
487 | formattedLogs.push(log);
488 | });
489 | // sorts logs in descending order
490 | formattedLogs.sort((a: Log, b: Log) => {
491 | return b.day_number - a.day_number;
492 | });
493 | return formattedLogs;
494 | }
495 |
496 | // joins milestones to each log
497 | async function addMilestonesToLogs(logs: Array) {
498 | // fetch all the milestones at once and then add them to each log iteratively below
499 | const milestoneData = getAllMilestones();
500 | if (logs && milestoneData) {
501 | const milestones = milestoneData.milestones;
502 | for (let log of logs) {
503 | const logMilestones = milestones.filter(n => n.day_number && n.day_number === log.day_number);
504 | if (logMilestones) {
505 | // extract the milestone ids
506 | const milestoneIds = logMilestones.map(n => n.id);
507 | log.milestones = Array.from(new Set(milestoneIds));
508 | }
509 | }
510 | }
511 |
512 | writeToLogsJson(logs);
513 | }
514 |
--------------------------------------------------------------------------------
/src/utils/MetricUtil.ts:
--------------------------------------------------------------------------------
1 | import { getSoftwareDir, isWindows, compareDates } from "./Util";
2 | import fs = require("fs");
3 | import { getFileDataAsJson } from "../managers/FileManager";
4 |
5 | function getSessionSummaryJson() {
6 | let file = getSoftwareDir();
7 | if (isWindows()) {
8 | file += "\\sessionSummary.json";
9 | } else {
10 | file += "/sessionSummary.json";
11 | }
12 | return file;
13 | }
14 |
15 | function getTimeCounterJson() {
16 | let file = getSoftwareDir();
17 | if (isWindows()) {
18 | file += "\\timeCounter.json";
19 | } else {
20 | file += "/timeCounter.json";
21 | }
22 | return file;
23 | }
24 |
25 | function getMinutesCoded(): number {
26 | const timeCounterFile = getTimeCounterJson();
27 | let minutes = 0;
28 | let timeCounterMetrics;
29 | try {
30 | // retries help when a user downloads Code Time with 100 Days of Code
31 | // they allow the file to be created and not throw errors
32 | let exists = false;
33 | let retries = 5;
34 | while (retries > 0 && !exists) {
35 | exists = fs.existsSync(timeCounterFile);
36 | retries--;
37 | }
38 | if (exists) {
39 | timeCounterMetrics = getFileDataAsJson(timeCounterFile);
40 | } else {
41 | return minutes;
42 | }
43 | if (!timeCounterMetrics) {
44 | timeCounterMetrics = {};
45 | }
46 | } catch (err) {
47 | return minutes;
48 | }
49 |
50 | // checks if file was updated today
51 | const day: string = timeCounterMetrics.current_day;
52 | const dayArr = day.split("-");
53 | const year = parseInt(dayArr[0]);
54 | const month = parseInt(dayArr[1]);
55 | const date = parseInt(dayArr[2]);
56 | const dateNow = new Date();
57 | if (year === dateNow.getFullYear() && month === dateNow.getMonth() + 1 && date === dateNow.getDate()) {
58 | // checks for avoiding null and undefined
59 | if (timeCounterMetrics.cumulative_code_time_seconds) {
60 | minutes = timeCounterMetrics.cumulative_code_time_seconds / 60;
61 | }
62 | }
63 | return minutes;
64 | }
65 |
66 | export function getSessionCodetimeMetrics(): any {
67 | const sessionSummaryFile = getSessionSummaryJson();
68 |
69 | let metricsOut = {
70 | minutes: getMinutesCoded(),
71 | keystrokes: 0,
72 | linesAdded: 0
73 | };
74 |
75 | // try to get codetime metrics from session summary file
76 | let metrics;
77 | try {
78 | // retries help when a user downloads Code Time with 100 Days of Code
79 | // they allow the file to be created and not throw errors
80 | let exists = false;
81 | let retries = 5;
82 | while (retries > 0 && !exists) {
83 | exists = fs.existsSync(sessionSummaryFile);
84 | retries--;
85 | }
86 | if (exists) {
87 | const stats = fs.statSync(sessionSummaryFile);
88 | // checks if file was updated today
89 | if (compareDates(new Date(), stats.mtime)) {
90 | metrics = getFileDataAsJson(sessionSummaryFile);
91 | } else {
92 | return metricsOut;
93 | }
94 | if (!metrics) {
95 | metrics = {};
96 | }
97 | } else {
98 | return metricsOut;
99 | }
100 | } catch (err) {
101 | return metricsOut;
102 | }
103 |
104 | // checks for avoiding null and undefined
105 | if (metrics.currentDayKeystrokes) {
106 | metricsOut.keystrokes = metrics.currentDayKeystrokes;
107 | }
108 | if (metrics.currentDayLinesAdded) {
109 | metricsOut.linesAdded = metrics.currentDayLinesAdded;
110 | }
111 |
112 | return metricsOut;
113 | }
114 |
--------------------------------------------------------------------------------
/src/utils/MilestonesTemplateUtil.ts:
--------------------------------------------------------------------------------
1 | import { window } from "vscode";
2 | import path = require("path");
3 | import fs = require("fs");
4 | import { getAllMilestones } from "./MilestonesUtil";
5 | import { compareDates, isLoggedIn } from "./Util";
6 | import { monthNames } from "./Constants";
7 |
8 | function getMilestonesTemplate(): string {
9 | return path.join(__dirname, "/assets/templates/milestones.template.html");
10 | }
11 |
12 | function generateShareUrl(id: number, title: string, description: string): string {
13 | const hashtagURI = "%23";
14 | const shareText = [`${title} - ${description}`, `\nWoohoo! I earned a new`].join("\n");
15 | const shareURI = encodeURI(shareText);
16 | const url = `https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.software.com%2Fmilestone%2F${id}&text=${shareURI}%20${hashtagURI}100DaysOfCode%20milestone%20via%20@software_hq's%20${hashtagURI}vscode%20extension`;
17 | return url;
18 | }
19 |
20 | function getStyleColorsBasedOnMode(): any {
21 | const tempWindow: any = window;
22 |
23 | let cardTextColor = "#FFFFFF";
24 | let cardBackgroundColor = "rgba(255,255,255,0.05)";
25 | let cardGrayedLevel = "#606060";
26 | let cardGrayedLevelFont = "#A2A2A2";
27 | if (tempWindow.activeColorTheme.kind === 1) {
28 | cardTextColor = "#444444";
29 | cardBackgroundColor = "rgba(0,0,0,0.10)";
30 | cardGrayedLevel = "#A2A2A2";
31 | cardGrayedLevelFont = "#606060";
32 | }
33 | return { cardTextColor, cardBackgroundColor, cardGrayedLevel, cardGrayedLevelFont };
34 | }
35 |
36 | export function getUpdatedMilestonesHtmlString(): string {
37 | const { cardTextColor, cardBackgroundColor, cardGrayedLevel, cardGrayedLevelFont } = getStyleColorsBasedOnMode();
38 |
39 | const milestoneData = getAllMilestones();
40 |
41 | // for adding to the html string later
42 | let recents: string = "";
43 | let allMilestones: string = "\n\t\t \n\t\tAll Milestones \n";
44 |
45 | // share icon
46 | if (milestoneData && milestoneData.milestones) {
47 | const milestones = milestoneData.milestones;
48 | for (let i = 0; i < milestones.length; i++) {
49 | const milestone = milestones[i];
50 | const id: number = milestone.id;
51 | const title: string = milestone.title;
52 | const description: string = milestone.description;
53 | const level: number = milestone.level;
54 | const achieved: boolean = milestone.achieved;
55 | const shareIcon: string = "https://100-days-of-code.s3-us-west-1.amazonaws.com/Milestones/share.svg";
56 |
57 | let icon: string;
58 | let dateAchieved: number = 0;
59 | const shareLink = generateShareUrl(i + 1, title, description);
60 | // If achieved, card must be colored. Otherwise, card should be gray
61 |
62 | // for adding gray scale effect class into html
63 | let grayedCard: string = "";
64 | let grayedLevel: string = "";
65 |
66 | // can only share if achieved
67 | let shareHtml: string = "";
68 |
69 | // can only have date if achieved
70 | let dateHtml: string = "";
71 |
72 | // Re: If achieved, card must be colored. Otherwise, card should be gray
73 | if (achieved) {
74 | icon = milestone.icon;
75 | dateAchieved = milestone.date_achieved;
76 | shareHtml = `\t\t\t `;
77 |
78 | // getting date in mm/dd/yyyy format
79 | let dateOb = new Date(dateAchieved);
80 |
81 | const dayNum = dateOb.getDate();
82 | const month = monthNames[dateOb.getMonth()];
83 | const year = dateOb.getFullYear();
84 |
85 | dateHtml = `\t\t\t${month} ${dayNum}, ${year}
`;
86 | } else {
87 | icon = milestone.gray_icon;
88 | grayedCard = "grayed";
89 | grayedLevel = "grayedLevel";
90 | }
91 |
92 | // if level 0, no level tag on top.
93 | // if level 6, replace it with ∞
94 | let levelHtml: string = "";
95 | if (level > 0 && level < 6) {
96 | levelHtml = `\t\t\tLevel ${level}
`;
97 | } else if (level === 6) {
98 | levelHtml = `\t\t\tLevel ∞
`;
99 | }
100 |
101 | const milestoneCardHtml: string = [
102 | `\t\t`,
103 | `\t\t\t
${id}
`,
104 | `${levelHtml}`,
105 | `${shareHtml}`,
106 | `\t\t\t
${title}
`,
107 | `\t\t\t
`,
108 | `\t\t\t
${description}
`,
109 | `${dateHtml}`,
110 | `\t\t
\n`
111 | ].join("\n");
112 |
113 | // Checks for the same date
114 | const dateNow = new Date();
115 | const dateOb = new Date(dateAchieved);
116 | if (compareDates(dateOb, dateNow)) {
117 | if (recents === "") {
118 | recents += `\n\t\tToday's Milestones \n`;
119 | }
120 | recents += milestoneCardHtml;
121 | }
122 |
123 | allMilestones += milestoneCardHtml;
124 | }
125 | }
126 |
127 | // If no milestones earned today
128 | if (recents === "") {
129 | recents += `\n\t\tToday's Milestones \n`;
130 | recents += `\t\tNo milestones earned today
\n`;
131 | }
132 |
133 | let logInVisibility = "hidden";
134 | let logInMessageDisplay = "none";
135 | if (!isLoggedIn()) {
136 | logInVisibility = "visible";
137 | logInMessageDisplay = "";
138 | }
139 |
140 | const templateVars = {
141 | cardTextColor,
142 | cardBackgroundColor,
143 | cardGrayedLevel,
144 | cardGrayedLevelFont,
145 | recents,
146 | allMilestones,
147 | logInVisibility,
148 | logInMessageDisplay
149 | };
150 |
151 | const templateString = fs.readFileSync(getMilestonesTemplate()).toString();
152 | const fillTemplate = function (templateString: string, templateVars: any) {
153 | return new Function("return `" + templateString + "`;").call(templateVars);
154 | };
155 |
156 | const milestoneHtmlContent = fillTemplate(templateString, templateVars);
157 | return milestoneHtmlContent;
158 | }
159 |
--------------------------------------------------------------------------------
/src/utils/MilestonesUtil.ts:
--------------------------------------------------------------------------------
1 | import { compareDates } from "./Util";
2 | import fs = require("fs");
3 | import { window, commands } from "vscode";
4 | import path = require("path");
5 | import { Summary } from "../models/Summary";
6 | import { updateSummaryMilestones, incrementSummaryShare, updateSummaryLanguages } from "./SummaryUtil";
7 | import { getLanguages } from "./LanguageUtil";
8 | import { HOURS_THRESHOLD } from "./Constants";
9 | import { Milestone } from "../models/Milestone";
10 | import { getFile, getFileDataAsJson, fetchSummaryJsonFileData } from "../managers/FileManager";
11 |
12 | export function getMilestonesJsonFilePath(): string {
13 | return getFile("milestones.json");
14 | }
15 |
16 | export function checkMilestonesJson(): boolean {
17 | const filePath = getMilestonesJsonFilePath();
18 | if (!fs.existsSync(filePath)) {
19 | try {
20 | const src = path.join(__dirname, "/assets/milestones.json");
21 | fs.copyFileSync(src, filePath);
22 | } catch (e) {
23 | return false;
24 | }
25 | }
26 | return true;
27 | }
28 |
29 | export function deleteMilestoneJson() {
30 | const filepath = getMilestonesJsonFilePath();
31 | const fileExists = fs.existsSync(filepath);
32 | if (fileExists) {
33 | fs.unlinkSync(filepath);
34 | }
35 | }
36 |
37 | export function getTodaysLocalMilestones(): Array {
38 | const now = new Date();
39 |
40 | // finds milestones achieved on date give and returns them
41 | const sendMilestones: Array = [];
42 | const milestoneData = getAllMilestones();
43 | if (milestoneData && milestoneData.milestones) {
44 | const milestones = milestoneData.milestones;
45 | for (let milestone of milestones) {
46 | if (milestone.achieved && milestone.date_achieved && compareDates(new Date(milestone.date_achieved), now)) {
47 | sendMilestones.push(milestone.id);
48 | }
49 | }
50 | }
51 | return sendMilestones;
52 | }
53 |
54 | export function compareWithLocalMilestones(serverMilestones: any) {
55 | // get the local copy of the milestones and update the attributes
56 | const localMilestoneData = getAllMilestones();
57 | const milestones = localMilestoneData.milestones || [];
58 |
59 | const hasServerMilestones = (serverMilestones && serverMilestones.length);
60 | for (let milestone of milestones) {
61 | if (hasServerMilestones) {
62 | const serverMilestone = serverMilestones.find((n:any) => n.milestones.includes(milestone.id));
63 | if (serverMilestone) {
64 | milestone.date_achieved = new Date(serverMilestone.unix_date * 1000);
65 | milestone.achieved = true;
66 | }
67 | }
68 | }
69 |
70 | writeToMilestoneJson(milestones);
71 | }
72 |
73 | export function checkCodeTimeMetricsMilestonesAchieved(): Array {
74 | let achievedMilestones = [];
75 | const summary: Summary = fetchSummaryJsonFileData();
76 |
77 | // check for aggregate codetime
78 | const aggHours = summary.hours + summary.currentHours;
79 | if (aggHours >= 200) {
80 | achievedMilestones.push(6, 5, 4, 3, 2, 1);
81 | } else if (aggHours >= 120) {
82 | achievedMilestones.push(5, 4, 3, 2, 1);
83 | } else if (aggHours >= 90) {
84 | achievedMilestones.push(4, 3, 2, 1);
85 | } else if (aggHours >= 60) {
86 | achievedMilestones.push(3, 2, 1);
87 | } else if (aggHours >= 30) {
88 | achievedMilestones.push(2, 1);
89 | } else if (aggHours >= 1) {
90 | achievedMilestones.push(1);
91 | }
92 |
93 | // check for daily codetime. These will be given out daily
94 | const dayHours = summary.currentHours;
95 | if (dayHours >= 10) {
96 | achievedMilestones.push(24, 23, 22, 21, 20, 19);
97 | } else if (dayHours >= 8) {
98 | achievedMilestones.push(23, 22, 21, 20, 19);
99 | } else if (dayHours >= 5) {
100 | achievedMilestones.push(22, 21, 20, 19);
101 | } else if (dayHours >= 3) {
102 | achievedMilestones.push(21, 20, 19);
103 | } else if (dayHours >= 2) {
104 | achievedMilestones.push(20, 19);
105 | } else if (dayHours >= 1) {
106 | achievedMilestones.push(19);
107 | }
108 |
109 | // check for lines added
110 | const lines = summary.lines_added + summary.currentLines;
111 | if (lines >= 10000) {
112 | achievedMilestones.push(30, 29, 28, 27, 26, 25);
113 | } else if (lines >= 1000) {
114 | achievedMilestones.push(29, 28, 27, 26, 25);
115 | } else if (lines >= 100) {
116 | achievedMilestones.push(28, 27, 26, 25);
117 | } else if (lines >= 50) {
118 | achievedMilestones.push(27, 26, 25);
119 | } else if (lines >= 16) {
120 | achievedMilestones.push(26, 25);
121 | } else if (lines >= 1) {
122 | achievedMilestones.push(25);
123 | }
124 |
125 | // check for keystrokes
126 | const keystrokes = summary.keystrokes + summary.currentKeystrokes;
127 | if (keystrokes >= 42195) {
128 | achievedMilestones.push(42, 41, 40, 39, 38, 37);
129 | } else if (keystrokes >= 21097) {
130 | achievedMilestones.push(41, 40, 39, 38, 37);
131 | } else if (keystrokes >= 10000) {
132 | achievedMilestones.push(40, 39, 38, 37);
133 | } else if (keystrokes >= 5000) {
134 | achievedMilestones.push(39, 38, 37);
135 | } else if (keystrokes >= 1000) {
136 | achievedMilestones.push(38, 37);
137 | } else if (keystrokes >= 100) {
138 | achievedMilestones.push(37);
139 | }
140 |
141 | if (achievedMilestones.length) {
142 | return achievedMilestonesJson(achievedMilestones);
143 | }
144 | return [];
145 | }
146 |
147 | export function checkLanguageMilestonesAchieved(): Array {
148 | updateSummaryLanguages();
149 | const summary: Summary = fetchSummaryJsonFileData();
150 | const languages = getLanguages();
151 | let milestones: Set = new Set();
152 |
153 | // single language check
154 | let language: string;
155 | for (language of languages) {
156 | switch (language) {
157 | case "c":
158 | case "cpp":
159 | milestones.add(51);
160 | break;
161 | case "html":
162 | case "css":
163 | milestones.add(54);
164 | break;
165 | case "javascript":
166 | case "javascriptreact":
167 | milestones.add(52);
168 | break;
169 | case "json":
170 | case "jsonc":
171 | milestones.add(55);
172 | break;
173 | case "java":
174 | milestones.add(49);
175 | break;
176 | case "plaintext":
177 | milestones.add(53);
178 | break;
179 | case "python":
180 | milestones.add(50);
181 | break;
182 | case "typescript":
183 | case "typescriptreact":
184 | milestones.add(56);
185 | break;
186 | }
187 | }
188 |
189 | // multi language check
190 | switch (summary.languages.length) {
191 | default:
192 | case 6:
193 | milestones.add(48);
194 | case 5:
195 | milestones.add(47);
196 | case 4:
197 | milestones.add(46);
198 | case 3:
199 | milestones.add(45);
200 | case 2:
201 | milestones.add(44);
202 | case 1:
203 | milestones.add(43);
204 | case 0:
205 | break;
206 | }
207 |
208 | const milestonesAchieved = Array.from(milestones);
209 | if (milestonesAchieved.length > 0) {
210 | return achievedMilestonesJson(milestonesAchieved);
211 | }
212 | return [];
213 | }
214 |
215 | export function checkDaysMilestones(): Array {
216 | const summary: Summary = fetchSummaryJsonFileData();
217 |
218 | let days = summary.days;
219 | let streaks = summary.longest_streak;
220 |
221 | // curr day is completed only after a certain threshold hours are met
222 | // no checks for prev day
223 | if (summary.currentHours < HOURS_THRESHOLD) {
224 | days--;
225 | streaks--;
226 | }
227 | let achievedMilestones = [];
228 |
229 | // checking for days
230 | if (days >= 110) {
231 | achievedMilestones.push(12);
232 | } else if (days >= 100) {
233 | achievedMilestones.push(11);
234 | } else if (days >= 75) {
235 | achievedMilestones.push(10);
236 | } else if (days >= 50) {
237 | achievedMilestones.push(9);
238 | } else if (days >= 10) {
239 | achievedMilestones.push(8);
240 | } else if (days >= 1) {
241 | achievedMilestones.push(7);
242 | }
243 |
244 | // checking for streaks
245 | if (streaks >= 100) {
246 | achievedMilestones.push(18);
247 | } else if (streaks >= 60) {
248 | achievedMilestones.push(17);
249 | } else if (streaks >= 30) {
250 | achievedMilestones.push(16);
251 | } else if (streaks >= 14) {
252 | achievedMilestones.push(15);
253 | } else if (streaks >= 7) {
254 | achievedMilestones.push(14);
255 | } else if (streaks >= 2) {
256 | achievedMilestones.push(13);
257 | }
258 |
259 | if (achievedMilestones.length > 0) {
260 | return achievedMilestonesJson(achievedMilestones);
261 | }
262 | return [];
263 | }
264 |
265 | export function checkSharesMilestones(): void {
266 | const summary: Summary = fetchSummaryJsonFileData();
267 | const shares = summary.shares;
268 |
269 | if (shares >= 100) {
270 | achievedMilestonesJson([36]);
271 | } else if (shares >= 50) {
272 | achievedMilestonesJson([35]);
273 | } else if (shares >= 21) {
274 | achievedMilestonesJson([34]);
275 | } else if (shares >= 10) {
276 | achievedMilestonesJson([33]);
277 | } else if (shares >= 5) {
278 | achievedMilestonesJson([32]);
279 | } else if (shares >= 1) {
280 | achievedMilestonesJson([31]);
281 | }
282 | }
283 |
284 | export function checkIfDaily(id: number): boolean {
285 | if ((id > 18 && id < 25) || (id > 48 && id < 57)) {
286 | return true;
287 | }
288 | return false;
289 | }
290 |
291 | function checkIdRange(id: number): boolean {
292 | const MIN_ID = 1;
293 | const MAX_ID = 56;
294 |
295 | if (id >= MIN_ID && id <= MAX_ID) {
296 | return true;
297 | }
298 | window.showErrorMessage("Incorrect Milestone Id! Please contact cody@software.com for help.");
299 | return false;
300 | }
301 |
302 | export function getMilestoneById(id: number): Milestone | any {
303 | if (!checkIdRange(id)) {
304 | return {};
305 | }
306 | const milestoneData = getAllMilestones();
307 | return milestoneData && milestoneData.milestones ? milestoneData.milestones.find((n: any) => n.id === id) : null;
308 | }
309 |
310 | function achievedMilestonesJson(ids: Array): Array {
311 | let updatedIds = [];
312 | const milestonesData = getAllMilestones();
313 | const milestones = milestonesData.milestones || [];
314 | const dateNow = new Date();
315 |
316 | let displayedPrompt = false;
317 | for (let id of ids) {
318 | const milestone = milestones.length > id - 1 ? milestones[id - 1] : null;
319 |
320 | // check if the milestone ID is valid and if the milestone has been marked as achieved or not
321 | if (!checkIdRange(id) || !milestone || (milestone.date_achieved && milestone.achieved)) {
322 | continue;
323 | }
324 |
325 | // this is the only place that sets "date_achieved" and "achieved" values
326 | milestone.achieved = true;
327 | milestone.date_achieved = dateNow.valueOf();
328 | updatedIds.push(id);
329 |
330 | if (id === 11) {
331 | displayedPrompt = true;
332 | const title = "View Dashboard";
333 | const msg = "Whoa! You just unlocked the #100DaysOfCode Certificate. Please view it on the 100 Days of Code Dashboard.";
334 | const commandCallback = "DoC.viewDashboard";
335 | commands.executeCommand("DoC.showInfoMessage", title, msg, true /*isModal*/, commandCallback);
336 | }
337 | }
338 |
339 | if (updatedIds.length) {
340 | // updates summary
341 | let totalMilestonesAchieved = 0;
342 | for (let i = 0; i < milestones.length; i++) {
343 | if (milestones[i].achieved) {
344 | totalMilestonesAchieved++;
345 | }
346 | }
347 | updateSummaryMilestones(updatedIds, totalMilestonesAchieved);
348 |
349 | // write to milestones file
350 | writeToMilestoneJson(milestones);
351 |
352 | if (!displayedPrompt) {
353 | const title = "View Milestones";
354 | const msg = "Hurray! You just achieved another milestone.";
355 | const commandCallback = "DoC.viewMilestones";
356 | commands.executeCommand("DoC.showInfoMessage", title, msg, false /*isModal*/, commandCallback);
357 | }
358 | return updatedIds;
359 | }
360 | return [];
361 | }
362 |
363 | export function updateMilestoneShare(id: number): void {
364 | if (!checkIdRange(id)) {
365 | return;
366 | }
367 | let milestones = getAllMilestones();
368 |
369 | // check and update milestones if not shared
370 | if (milestones && milestones.length && milestones[id - 1] && !milestones[id - 1].shared) {
371 | milestones[id - 1].shared = true;
372 | writeToMilestoneJson(milestones);
373 | incrementSummaryShare();
374 | }
375 | }
376 |
377 | export function getTotalMilestonesAchieved(): number {
378 | const milestoneData = getAllMilestones();
379 | const milestones = milestoneData.milestones || [];
380 | let totalMilestonesAchieved = 0;
381 | for (let milestone of milestones) {
382 | if (milestone.achieved) {
383 | totalMilestonesAchieved++;
384 | }
385 | }
386 | return totalMilestonesAchieved;
387 | }
388 |
389 | /**
390 | * This returns the milestones data
391 | * {milestones: []}
392 | */
393 | export function getAllMilestones(): any {
394 | // Checks if the file exists and if not, creates a new file
395 | if (!checkMilestonesJson()) {
396 | window.showErrorMessage("Cannot access Milestone file! Please contact cody@software.com for help.");
397 | return { milestones: [] };
398 | }
399 | const filepath = getMilestonesJsonFilePath();
400 | const milestoneData = getFileDataAsJson(filepath);
401 | return milestoneData || { milestones: [] };
402 | }
403 |
404 | export function getThreeMostRecentMilestones(): Array {
405 | const milestoneData = getAllMilestones();
406 | const milestones = milestoneData.milestones || [];
407 | milestones.sort((a: any, b: any) => {
408 | // sorting in descending order of date_achieved
409 | if (a.achieved && b.achieved) {
410 | return b.date_achieved - a.date_achieved;
411 | } else if (a.achieved) {
412 | return -1;
413 | } else if (b.achieved) {
414 | return 1;
415 | } else {
416 | return 0;
417 | }
418 | });
419 | let sendMilestones: Array = [];
420 | const rawSendMilestones = milestones.slice(0, 3);
421 | rawSendMilestones.forEach((milestone: any) => {
422 | if (milestone.achieved) {
423 | sendMilestones.push(milestone.id);
424 | }
425 | });
426 | return sendMilestones;
427 | }
428 |
429 | function writeToMilestoneJson(milestones: Array) {
430 | const filepath = getMilestonesJsonFilePath();
431 | let sendMilestones = { milestones };
432 | try {
433 | fs.writeFileSync(filepath, JSON.stringify(sendMilestones, null, 2));
434 | } catch (err) {
435 | console.log(err);
436 | }
437 | }
438 |
--------------------------------------------------------------------------------
/src/utils/PluginUtil.ts:
--------------------------------------------------------------------------------
1 | import { extensions } from "vscode";
2 | import { _100_DAYS_OF_CODE_EXT_ID, _100_DAYS_OF_CODE_PLUGIN_ID } from "./Constants";
3 | const os = require("os");
4 |
5 | export function getPluginId() {
6 | return _100_DAYS_OF_CODE_PLUGIN_ID;
7 | }
8 |
9 | export function getPluginName() {
10 | return _100_DAYS_OF_CODE_EXT_ID;
11 | }
12 |
13 | export function getVersion() {
14 | const extension = extensions.getExtension(_100_DAYS_OF_CODE_EXT_ID);
15 | if (extension) {
16 | return extension.packageJSON.version;
17 | } else {
18 | return;
19 | }
20 | }
21 |
22 | export function getOs() {
23 | let parts = [];
24 | let osType = os.type();
25 | if (osType) {
26 | parts.push(osType);
27 | }
28 | let osRelease = os.release();
29 | if (osRelease) {
30 | parts.push(osRelease);
31 | }
32 | let platform = os.platform();
33 | if (platform) {
34 | parts.push(platform);
35 | }
36 | if (parts.length > 0) {
37 | return parts.join("_");
38 | }
39 | return "";
40 | }
41 |
42 | export function getOffsetSeconds() {
43 | let d = new Date();
44 | return d.getTimezoneOffset() * 60;
45 | }
--------------------------------------------------------------------------------
/src/utils/SummaryUtil.ts:
--------------------------------------------------------------------------------
1 | import { compareDates, getItem, mergeStringArrays, resetData } from "./Util";
2 | import fs = require("fs");
3 | import { getMostRecentLogObject, checkIfOnStreak, getLogsSummary, createLog } from "./LogsUtil";
4 | import { getLanguages } from "./LanguageUtil";
5 | import { Summary } from "../models/Summary";
6 | import { Log } from "../models/Log";
7 | import { getTotalMilestonesAchieved, getThreeMostRecentMilestones } from "./MilestonesUtil";
8 | import { getSummaryJsonFilePath, fetchSummaryJsonFileData } from "../managers/FileManager";
9 | import { commands } from "vscode";
10 |
11 | let challenge_round = -1;
12 |
13 | export function getCurrentChallengeRound() {
14 | if (challenge_round === -1) {
15 | // fetch it from the local summary
16 | let summary: Summary = fetchSummaryJsonFileData();
17 | challenge_round = summary.challenge_round;
18 | }
19 | return challenge_round;
20 | }
21 |
22 | export async function restartChallenge() {
23 | // increment the challenge round
24 | challenge_round += 1;
25 |
26 | // reset the data
27 | resetData();
28 |
29 | // set the challenge round in the local summary
30 | const summary:Summary = new Summary();
31 | summary.challenge_round = challenge_round;
32 | writeToSummaryJson(summary);
33 |
34 | // create a log with the new challenge round
35 | const log:Log = new Log();
36 | log.day_number = 1;
37 | log.challenge_round = challenge_round;
38 |
39 | await createLog(log);
40 |
41 | // show the dashboard view
42 | commands.executeCommand("DoC.viewDashboard");
43 | }
44 |
45 | export function deleteSummaryJson() {
46 | const filepath = getSummaryJsonFilePath();
47 | const fileExists = fs.existsSync(filepath);
48 | if (fileExists) {
49 | fs.unlinkSync(filepath);
50 | }
51 | }
52 |
53 | export function syncSummary() {
54 | console.log("Syncing 100 doc summary");
55 | // Aggregating log data
56 | const aggregateLogData = getLogsSummary();
57 |
58 | // Aggregating milestone data
59 | const totalMilestones = getTotalMilestonesAchieved();
60 |
61 | let summary: Summary = fetchSummaryJsonFileData();
62 | //aggregate hours has the total hours in the logs, we need to subtract the current day's hours because they are added at the end of the day.
63 | summary.hours = aggregateLogData.totalHours;
64 | summary.lines_added = aggregateLogData.totalLinesAdded;
65 | summary.keystrokes = aggregateLogData.totalKeystrokes;
66 | summary.currentHours = aggregateLogData.currentHours;
67 | summary.currentKeystrokes = aggregateLogData.currentKeystrokes;
68 | summary.currentLines = aggregateLogData.currentLines;
69 |
70 | summary.days = aggregateLogData.totalDays;
71 | summary.longest_streak = aggregateLogData.longest_streak;
72 | summary.current_streak = aggregateLogData.current_streak;
73 |
74 | summary.milestones = totalMilestones;
75 | summary.recent_milestones = getThreeMostRecentMilestones();
76 |
77 | summary.currentDate = aggregateLogData.currentDate;
78 |
79 | writeToSummaryJson(summary);
80 | }
81 |
82 | export function updateSummaryJson() {
83 | let summary: Summary = fetchSummaryJsonFileData();
84 | const log: Log = getMostRecentLogObject();
85 | const onStreak = checkIfOnStreak();
86 | const currentDate = new Date(summary.currentDate);
87 | const dateOb = new Date();
88 |
89 | // if current date is not today, update aggregate data
90 | if (!compareDates(dateOb, currentDate)) {
91 | summary.days += 1;
92 | summary.hours += summary.currentHours;
93 | summary.keystrokes += summary.currentKeystrokes;
94 | summary.lines_added += summary.currentLines;
95 | summary.currentDate = dateOb.valueOf();
96 |
97 | if (onStreak) {
98 | summary.current_streak += 1;
99 | if (summary.current_streak > summary.longest_streak) {
100 | summary.longest_streak = summary.current_streak;
101 | }
102 | } else {
103 | summary.current_streak = 1;
104 | }
105 | }
106 |
107 | // update day's data
108 | summary.currentHours = log.codetime_metrics.hours;
109 | summary.currentKeystrokes = log.codetime_metrics.keystrokes;
110 | summary.currentLines = log.codetime_metrics.lines_added;
111 |
112 | // update languages aggregate and make sure none are repeated
113 | const newLanguages = getLanguages();
114 | if (newLanguages) {
115 | const currLanguages = summary.languages || [];
116 | const totalLanguages = currLanguages.concat(newLanguages);
117 | const reducedLanguages = Array.from(new Set(totalLanguages));
118 | summary.languages = reducedLanguages;
119 | }
120 | summary.lastUpdated = new Date().getTime();
121 | writeToSummaryJson(summary);
122 | }
123 |
124 | export function updateSummaryMilestones(newMilestones: Array, totalMilestones: number) {
125 | let summary: Summary = fetchSummaryJsonFileData();
126 | summary.milestones = totalMilestones;
127 |
128 | // order milestones in latest to oldest order of achievement
129 | summary.recent_milestones = newMilestones.reverse().concat(summary.recent_milestones);
130 | // limit milestones to 3 for displaying on the dashboard
131 | while (summary.recent_milestones.length > 3) {
132 | summary.recent_milestones.pop();
133 | }
134 | summary.lastUpdated = new Date().getTime();
135 | writeToSummaryJson(summary);
136 | }
137 |
138 | export function getSummaryTotalHours() {
139 | let summary: Summary = fetchSummaryJsonFileData();
140 | return summary.hours;
141 | }
142 |
143 | export function setSummaryTotalHours(newHours: number) {
144 | let summary: Summary = fetchSummaryJsonFileData();
145 | summary.hours = newHours;
146 | writeToSummaryJson(summary);
147 | }
148 |
149 | export function setSummaryCurrentHours(newCurrentHours: number) {
150 | let summary: Summary = fetchSummaryJsonFileData();
151 | summary.currentHours = newCurrentHours;
152 | writeToSummaryJson(summary);
153 | }
154 |
155 | export function updateSummaryLanguages() {
156 | // update languages aggregate and make sure none are repeated
157 | const newLanguages = getLanguages();
158 | let summary: Summary = fetchSummaryJsonFileData();
159 | const currLanguages = summary.languages;
160 | const totalLanguages = currLanguages.concat(newLanguages);
161 | const reducedLanguages = Array.from(new Set(totalLanguages));
162 | summary.languages = reducedLanguages;
163 | summary.lastUpdated = new Date().getTime();
164 | writeToSummaryJson(summary);
165 | }
166 |
167 | export function incrementSummaryShare() {
168 | const summary: Summary = fetchSummaryJsonFileData();
169 | summary.shares++;
170 | writeToSummaryJson(summary);
171 | }
172 |
173 | export function getDaysLevel(daysComplete: number): any {
174 | // based on days milestones
175 | let daysLevel = 0;
176 | let daysProgressPercentage = 0;
177 | if (daysComplete >= 110) {
178 | daysLevel = 6;
179 | daysProgressPercentage = 100;
180 | } else if (daysComplete >= 100) {
181 | daysLevel = 5;
182 | daysProgressPercentage = ((daysComplete - 100) * 100) / (110 - 100);
183 | } else if (daysComplete >= 75) {
184 | daysLevel = 4;
185 | daysProgressPercentage = ((daysComplete - 75) * 100) / (100 - 75);
186 | } else if (daysComplete >= 50) {
187 | daysLevel = 3;
188 | daysProgressPercentage = ((daysComplete - 50) * 100) / (75 - 50);
189 | } else if (daysComplete >= 10) {
190 | daysLevel = 2;
191 | daysProgressPercentage = ((daysComplete - 10) * 100) / (50 - 10);
192 | } else if (daysComplete >= 1) {
193 | daysLevel = 1;
194 | daysProgressPercentage = ((daysComplete - 1) * 100) / (10 - 1);
195 | } else {
196 | daysLevel = 0;
197 | daysProgressPercentage = (daysComplete * 100) / (1 - 0);
198 | }
199 | return { daysLevel, daysProgressPercentage };
200 | }
201 |
202 | export function getHoursLevel(hoursCoded: number): any {
203 | // based on hours milestones
204 | let hoursLevel = 0;
205 | let hoursProgressPercentage = 0;
206 | if (hoursCoded >= 200) {
207 | hoursLevel = 6;
208 | hoursProgressPercentage = 100;
209 | } else if (hoursCoded >= 120) {
210 | hoursLevel = 5;
211 | hoursProgressPercentage = ((hoursCoded - 120) * 100) / (200 - 120);
212 | } else if (hoursCoded >= 90) {
213 | hoursLevel = 4;
214 | hoursProgressPercentage = ((hoursCoded - 90) * 100) / (120 - 90);
215 | } else if (hoursCoded >= 60) {
216 | hoursLevel = 3;
217 | hoursProgressPercentage = ((hoursCoded - 60) * 100) / (90 - 60);
218 | } else if (hoursCoded >= 30) {
219 | hoursLevel = 2;
220 | hoursProgressPercentage = ((hoursCoded - 30) * 100) / (60 - 30);
221 | } else if (hoursCoded >= 1) {
222 | hoursLevel = 1;
223 | hoursProgressPercentage = ((hoursCoded - 1) * 100) / (30 - 1);
224 | } else {
225 | hoursLevel = 0;
226 | hoursProgressPercentage = (hoursCoded * 100) / (1 - 0);
227 | }
228 | return { hoursLevel, hoursProgressPercentage };
229 | }
230 |
231 | export function getLongStreakLevel(longestStreak: number): any {
232 | // based on streaks milestones
233 | let streaksLevel = 0;
234 | let streaksProgressPercentage = 0;
235 | if (longestStreak >= 100) {
236 | streaksLevel = 6;
237 | streaksProgressPercentage = 100;
238 | } else if (longestStreak >= 60) {
239 | streaksLevel = 5;
240 | streaksProgressPercentage = ((longestStreak - 60) * 100) / (100 - 60);
241 | } else if (longestStreak >= 30) {
242 | streaksLevel = 4;
243 | streaksProgressPercentage = ((longestStreak - 30) * 100) / (60 - 30);
244 | } else if (longestStreak >= 14) {
245 | streaksLevel = 3;
246 | streaksProgressPercentage = ((longestStreak - 14) * 100) / (30 - 14);
247 | } else if (longestStreak >= 7) {
248 | streaksLevel = 2;
249 | streaksProgressPercentage = ((longestStreak - 7) * 100) / (14 - 7);
250 | } else if (longestStreak >= 2) {
251 | streaksLevel = 1;
252 | streaksProgressPercentage = ((longestStreak - 2) * 100) / (7 - 2);
253 | } else {
254 | streaksLevel = 0;
255 | streaksProgressPercentage = (longestStreak * 100) / (2 - 0);
256 | }
257 | return { streaksLevel, streaksProgressPercentage };
258 | }
259 |
260 | export function getLinesAddedLevel(linesAdded: number): any {
261 | // based on number of lines added milestones
262 | let linesAddedLevel = 0;
263 | let linesAddedProgressPercentage = 0;
264 | if (linesAdded >= 10000) {
265 | linesAddedLevel = 6;
266 | linesAddedProgressPercentage = 100;
267 | } else if (linesAdded >= 1000) {
268 | linesAddedLevel = 5;
269 | linesAddedProgressPercentage = ((linesAdded - 1000) * 100) / (10000 - 1000);
270 | } else if (linesAdded >= 100) {
271 | linesAddedLevel = 4;
272 | linesAddedProgressPercentage = ((linesAdded - 100) * 100) / (1000 - 100);
273 | } else if (linesAdded >= 50) {
274 | linesAddedLevel = 3;
275 | linesAddedProgressPercentage = ((linesAdded - 50) * 100) / (100 - 50);
276 | } else if (linesAdded >= 16) {
277 | linesAddedLevel = 2;
278 | linesAddedProgressPercentage = ((linesAdded - 16) * 100) / (50 - 16);
279 | } else if (linesAdded >= 1) {
280 | linesAddedLevel = 1;
281 | linesAddedProgressPercentage = ((linesAdded - 1) * 100) / (16 - 1);
282 | } else {
283 | linesAddedLevel = 0;
284 | linesAddedProgressPercentage = (linesAdded * 100) / (1 - 0);
285 | }
286 | return { linesAddedLevel, linesAddedProgressPercentage };
287 | }
288 |
289 | export function getAverageHoursLevel(avgHour: number): number {
290 | // based on avg hours for 100 days of code
291 | if (avgHour >= 3.0) {
292 | return 6;
293 | } else if (avgHour >= 2.5) {
294 | return 5;
295 | } else if (avgHour >= 2.0) {
296 | return 4;
297 | } else if (avgHour >= 1.5) {
298 | return 3;
299 | } else if (avgHour >= 1.0) {
300 | return 2;
301 | } else if (avgHour >= 0.5) {
302 | return 1;
303 | } else {
304 | return 0;
305 | }
306 | }
307 |
308 | function writeToSummaryJson(summary: Summary) {
309 | const filepath = getSummaryJsonFilePath();
310 | try {
311 | fs.writeFileSync(filepath, JSON.stringify(summary, null, 2));
312 | } catch (err) {
313 | console.log(err);
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/utils/Util.ts:
--------------------------------------------------------------------------------
1 | import { commands, ViewColumn, Uri, window } from "vscode";
2 | import { _100_DAYS_OF_CODE_PLUGIN_ID, _100_DAYS_OF_CODE_EXT_ID } from "./Constants";
3 | import { deleteLogsJson, syncLogs } from "./LogsUtil";
4 | import { reloadCurrentView } from "./CommandUtil";
5 | import { MilestoneEventManager } from "../managers/MilestoneEventManager";
6 | import { deleteMilestoneJson } from "./MilestonesUtil";
7 | import { deleteSummaryJson } from "./SummaryUtil";
8 |
9 | const moment = require("moment-timezone");
10 | const fileIt = require("file-it");
11 | const fs = require("fs");
12 | const os = require("os");
13 |
14 | let check_logon_interval: NodeJS.Timeout;
15 | let check_login_interval_max_times = 40;
16 | let current_check_login_interval_count = 0;
17 | let checking_login = false;
18 | let _name = "";
19 |
20 |
21 | export function isWindows() {
22 | return process.platform.indexOf("win32") !== -1;
23 | }
24 |
25 | export function isMac() {
26 | return process.platform.indexOf("darwin") !== -1;
27 | }
28 |
29 | export function getSoftwareDir(autoCreate = true) {
30 | const homedir = os.homedir();
31 | let softwareDataDir = homedir;
32 | if (isWindows()) {
33 | softwareDataDir += "\\.software";
34 | } else {
35 | softwareDataDir += "/.software";
36 | }
37 |
38 | if (autoCreate && !fs.existsSync(softwareDataDir)) {
39 | fs.mkdirSync(softwareDataDir);
40 | }
41 |
42 | return softwareDataDir;
43 | }
44 |
45 | export function getSoftwareSessionFile() {
46 | let file = getSoftwareDir();
47 | if (isWindows()) {
48 | file += "\\session.json";
49 | } else {
50 | file += "/session.json";
51 | }
52 | return file;
53 | }
54 |
55 | export function compareDates(day1: Date, day2: Date) {
56 | return (
57 | day1.getDate() === day2.getDate() &&
58 | day1.getMonth() === day2.getMonth() &&
59 | day1.getFullYear() === day2.getFullYear()
60 | );
61 | }
62 |
63 | export function getLocalREADMEFile() {
64 | let file = __dirname;
65 | // the readme is one directory above the util
66 | if (isWindows()) {
67 | file += "\\..\\README.md";
68 | } else {
69 | file += "/../README.md";
70 | }
71 | return file;
72 | }
73 |
74 | export function displayReadmeIfNotExists(override = false) {
75 | const displayedReadme = getItem("vscode_100doc_CtReadme");
76 | if (!displayedReadme || override) {
77 | const readmeUri = Uri.file(getLocalREADMEFile());
78 | commands.executeCommand("markdown.showPreview", readmeUri, ViewColumn.One, { locked: true });
79 | setItem("vscode_100doc_CtReadme", true);
80 |
81 | commands.executeCommand("DoC.revealTree");
82 | }
83 | }
84 |
85 | export function getItem(key: string) {
86 | const jsonObj = getSoftwareSessionAsJson();
87 | let val = jsonObj[key] || null;
88 | return val;
89 | }
90 |
91 | export function setItem(key: string, value: any) {
92 | fileIt.setJsonValue(getSoftwareSessionFile(), key, value);
93 | }
94 |
95 | export function getJwt(prefix = false) {
96 | const jwt = getItem("jwt");
97 | if (!jwt || prefix) {
98 | return jwt;
99 | } else {
100 | return jwt.split("JWT ")[1];
101 | }
102 | }
103 |
104 | export function getSoftwareSessionAsJson() {
105 | let data = null;
106 |
107 | const sessionFile = getSoftwareSessionFile();
108 | if (fs.existsSync(sessionFile)) {
109 | const content = fs.readFileSync(sessionFile).toString();
110 | if (content) {
111 | try {
112 | data = JSON.parse(content);
113 | } catch (e) {
114 | console.log(`unable to read session info: ${e.message}`);
115 | data = {};
116 | }
117 | }
118 | }
119 | return data ? data : {};
120 | }
121 |
122 | export function isLoggedIn(): boolean {
123 | // getting authType to see if user is logged in. name is a check for if the user has not successfully logged in.
124 | if (getItem("name")) {
125 | _name = getItem("name");
126 | return true;
127 | }
128 | return false;
129 | }
130 |
131 | export function displayLoginPromptIfNotLoggedIn() {
132 | const lastPromptDate = getItem("last100doc_loginPromptDate");
133 | const today = moment().format("YYYY-MM-DD");
134 | if (!isLoggedIn() && lastPromptDate !== today) {
135 | setItem("last100doc_loginPromptDate", today);
136 | window
137 | .showInformationMessage(
138 | "You must log in with Code Time to start tracking your 100 Days of Code.",
139 | {
140 | modal: true
141 | },
142 | "Log in"
143 | )
144 | .then(async selection => {
145 | if (selection === "Log in") {
146 | const items = [
147 | { label: "Log in with Google" },
148 | { label: "Log in with GitHub" },
149 | { label: "Log in with Email" }
150 | ];
151 | const selection = await window.showQuickPick(items);
152 | switch (selection?.label) {
153 | case "Log in with Google":
154 | commands.executeCommand("codetime.googleLogin");
155 | break;
156 | case "Log in with GitHub":
157 | commands.executeCommand("codetime.githubLogin");
158 | break;
159 | default:
160 | commands.executeCommand("codetime.codeTimeLogin");
161 | break;
162 | }
163 |
164 | if (!checking_login) {
165 | setTimeout(() => initializeLogInCheckInterval(), 8000);
166 | }
167 | }
168 | });
169 | }
170 | }
171 |
172 | function initializeLogInCheckInterval() {
173 | checking_login = true;
174 | check_logon_interval = setInterval(async () => {
175 | const loggedIn = isLoggedIn();
176 | const passedTimeThreshold = current_check_login_interval_count >= check_login_interval_max_times;
177 |
178 | if (loggedIn) {
179 | checking_login = false;
180 | clearInterval(check_logon_interval);
181 | rebuildData()
182 | } else if (passedTimeThreshold) {
183 | checking_login = false;
184 | clearInterval(check_logon_interval);
185 | }
186 | current_check_login_interval_count++;
187 | }, 10000);
188 | }
189 |
190 | export async function checkIfNameChanged() {
191 | const name = getItem("name");
192 | if (_name !== name) {
193 | _name = name;
194 |
195 | await rebuildData();
196 |
197 | return true;
198 | } else {
199 | return false;
200 | }
201 | }
202 |
203 | export async function rebuildData() {
204 |
205 | resetData();
206 |
207 | window.showInformationMessage("Loading account logs and milestones...");
208 |
209 | await Promise.all([
210 | syncLogs(),
211 | MilestoneEventManager.getInstance().fetchMilestones(),
212 | ]);
213 |
214 | reloadCurrentView();
215 | }
216 |
217 | export async function resetData() {
218 | // reset files
219 | deleteMilestoneJson();
220 | deleteLogsJson();
221 | deleteSummaryJson();
222 | }
223 |
224 | export function mergeStringArrays(array1: string[], array2: string[]) {
225 | array1 = array1 || [];
226 | array2 = array2 || [];
227 | return [...new Set([...array1, ...array2])];
228 | }
229 |
230 | export function formatNumber(num) {
231 | let str = "";
232 | num = num ? parseFloat(num) : 0;
233 | if (num >= 1000) {
234 | str = num.toLocaleString();
235 | } else if (num % 1 === 0) {
236 | str = num.toFixed(0);
237 | } else {
238 | str = num.toFixed(2);
239 | }
240 | return str;
241 | }
242 |
243 | export function getMillisSinceLastUpdate(file) {
244 | if (!fs.existsSync(file)) {
245 | return -1;
246 | }
247 | const stats = fs.statSync(file);
248 |
249 | return (new Date().getTime() - stats.mtime);
250 | }
251 |
252 | export function getInputFormStyles() {
253 | let cardTextColor = "#FFFFFF";
254 | let cardBackgroundColor = "rgba(255,255,255,0.05)";
255 | let cardGrayedLevel = "#474747";
256 | let sharePath = "https://100-days-of-code.s3-us-west-1.amazonaws.com/Milestones/share.svg";
257 | if (window.activeColorTheme.kind === 1) {
258 | cardTextColor = "#444444";
259 | cardBackgroundColor = "rgba(0,0,0,0.10)";
260 | cardGrayedLevel = "#B5B5B5";
261 | sharePath = "https://100-days-of-code.s3-us-west-1.amazonaws.com/Milestones/shareLight.svg";
262 | }
263 | return { cardTextColor, cardBackgroundColor, cardGrayedLevel, sharePath };
264 | }
265 |
--------------------------------------------------------------------------------
/swdc-100-days-of-code-1.2.1.vsix:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode-100-days-of-code/5e74bb622660543701e05dbfb37408a504e95d9c/swdc-100-days-of-code-1.2.1.vsix
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "./dist/",
6 | "lib": [
7 | "es6"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "."
11 | },
12 | "exclude": ["node_modules", ".vscode-test"]
13 | }
14 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | //@ts-check
2 |
3 | "use strict";
4 |
5 | const path = require("path");
6 | const CopyPlugin = require("copy-webpack-plugin");
7 |
8 | const config = {
9 | target: "node", // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
10 | output: {
11 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
12 | path: path.resolve(__dirname, "dist"),
13 | filename: "extension.js",
14 | libraryTarget: "commonjs2",
15 | devtoolModuleFilenameTemplate: "../[resource-path]",
16 | },
17 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
18 | node: {
19 | __dirname: false,
20 | __filename: false,
21 | },
22 | devtool: "source-map",
23 | externals: {
24 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
25 | },
26 | resolve: {
27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
28 | extensions: [".ts", ".js"],
29 | },
30 | plugins: [
31 | new CopyPlugin({
32 | patterns: [
33 | { from: "./resources", to: "resources" },
34 | { from: "./assets", to: "assets" },
35 | { from: "./README.md", to: "resources" },
36 | ],
37 | }),
38 | ],
39 | module: {
40 | rules: [
41 | {
42 | test: /\.ts$/,
43 | loader: "ts-loader",
44 | options: { allowTsInNodeModules: true },
45 | },
46 | {
47 | test: /\.(png|svg|jpg|gif)$/,
48 | use: ["file-loader"],
49 | },
50 | ],
51 | },
52 | };
53 | module.exports = config;
54 |
--------------------------------------------------------------------------------