Compare commits

...

199 Commits

Author SHA1 Message Date
Elio Struyf
f46419f800 Merge branch 'dev' into main 2021-09-22 20:06:44 +02:00
Elio Struyf
94a89be841 Updated changelog 2021-09-22 20:06:27 +02:00
Elio Struyf
00224e99a0 Merge branch 'dev' into main 2021-09-22 20:03:56 +02:00
Elio Struyf
cfd6eb6fb3 Updated settings 2021-09-22 10:09:41 +02:00
Elio Struyf
701c5ccb9e #111 #112 - media insert + snippet support 2021-09-22 09:12:21 +02:00
Elio Struyf
bf797df982 #111 - Insert image into the contents of the page 2021-09-21 20:18:24 +02:00
Elio Struyf
0f710ffefb Moved documentation to another repository 2021-09-21 18:54:45 +02:00
Elio Struyf
2a87f08287 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-09-21 09:25:44 +02:00
Elio Struyf
c27c5885b1 Setting updates 2021-09-21 09:01:59 +02:00
Elio Struyf
e5771b0fc9 Update banners 2021-09-20 21:41:57 +02:00
Elio Struyf
c9a5232d43 Updated banner 2021-09-20 21:38:15 +02:00
Elio Struyf
266faf9081 updated readme 2021-09-20 21:36:31 +02:00
Elio Struyf
cdf3dc7f2e Updated foldername 2021-09-20 17:22:58 +02:00
Elio Struyf
1bcfa9d9f0 Updated readme 2021-09-20 17:22:26 +02:00
Elio Struyf
d2dd4aed37 Added github stars icon 2021-09-20 15:29:06 +02:00
Elio Struyf
6eea02622a Link to the updates page updates 2021-09-20 15:06:11 +02:00
Elio Struyf
395b5da765 Anchor improvements to the documentation 2021-09-20 14:30:20 +02:00
Elio Struyf
635f033193 Added release notes notification 2021-09-20 12:32:49 +02:00
Elio Struyf
14b100ea20 4.0.0 2021-09-20 12:29:40 +02:00
Elio Struyf
693cc3611c Show release notes 2021-09-20 12:29:15 +02:00
Elio Struyf
a9591e1a56 Release notes 2021-09-20 12:22:58 +02:00
Elio Struyf
7a702f09a0 Documenation updates and preparations for 4.0.0 release 2021-09-20 11:23:34 +02:00
Elio Struyf
04ccdbb646 Sorted all settings 2021-09-20 09:38:18 +02:00
Elio Struyf
b2e661d9e3 Beta schema 2021-09-20 09:35:43 +02:00
Elio Struyf
d2161ff46c Sorted all commands 2021-09-20 09:35:37 +02:00
Elio Struyf
8e699db62d #106 - new init for team config 2021-09-19 18:54:38 +02:00
Elio
25201e6859 Updated script name 2021-09-18 16:19:44 +02:00
Elio
c1c14b814c added schema support on build 2021-09-18 16:18:46 +02:00
Elio Struyf
001a49b34e Global/local settings 2021-09-17 20:26:52 +02:00
Elio Struyf
08fcf7cf15 Global/local settings implementation 2021-09-17 17:31:37 +02:00
Elio Struyf
78db3d0b96 #108 - Choice field support added 2021-09-17 14:04:06 +02:00
Elio Struyf
ed460fde4f Documentation updates 2021-09-17 11:42:03 +02:00
Elio Struyf
1471203b68 updated changelog 2021-09-17 10:38:12 +02:00
Elio Struyf
53093ba459 Documentation updates 2021-09-17 10:36:09 +02:00
Elio Struyf
f873961439 Panel aware media selection 2021-09-17 08:46:48 +02:00
Elio Struyf
5146bd04df Added label support to tagpicker 2021-09-16 19:47:41 +02:00
Elio Struyf
9247884a5b #105 - Content Type support with backwards compatibility 2021-09-16 16:20:58 +02:00
Elio Struyf
e159a6239c Mapped out all array and object settings 2021-09-16 14:42:53 +02:00
Elio Struyf
626cc773f4 #104 - First steps to preview image selection 2021-09-16 11:46:37 +02:00
Elio Struyf
1e26329d38 #101 - Added now button to datetime 2021-09-16 09:06:50 +02:00
Elio Struyf
58e0b4bde5 #103 - Title and description field added to metadata 2021-09-15 18:53:42 +02:00
Elio Struyf
b072b920fd #102 - Fix on metadata update 2021-09-15 18:41:27 +02:00
Elio Struyf
2758c94434 Added new setting to documentation 2021-09-15 17:12:37 +02:00
Elio Struyf
83c086879d Updated changelog 2021-09-15 16:43:01 +02:00
Elio Struyf
0429eaa9ed #102 - Support comma separated arrays in front matter 2021-09-15 16:42:07 +02:00
Elio Struyf
73c35e3b44 Updated changelog 2021-09-15 13:20:09 +02:00
Elio Struyf
089f21ca72 3.2.0 2021-09-15 13:19:12 +02:00
Elio Struyf
0efdb019a0 #101 - Date picker added to metadata 2021-09-15 13:19:03 +02:00
Elio Struyf
57623378fb Remove sticky from sidebar 2021-09-14 08:51:35 +02:00
Elio Struyf
27566da617 Added 404 page 2021-09-13 16:56:30 +02:00
Elio Struyf
c429113394 Updated docs 2021-09-13 10:16:57 +02:00
Elio Struyf
2e12942e22 Created robots.txt 2021-09-11 15:25:56 +02:00
Elio Struyf
fa5b698571 Updated changelog 2021-09-11 15:11:44 +02:00
Elio Struyf
4237569390 Added cloudinary 2021-09-11 09:17:03 +02:00
Elio Struyf
01b1b3d0ea Merge pull request #100 from estruyf/dev
Merge for 3.1.0
2021-09-10 17:43:27 +02:00
Elio Struyf
9f2c68fd66 Updated showcase 2021-09-10 16:56:20 +02:00
Elio Struyf
9f21042e58 Updated documentations with delete and drag&drop 2021-09-10 14:07:33 +02:00
Elio Struyf
840e6fc6fd Delete media 2021-09-10 14:01:46 +02:00
Elio Struyf
3f237386b2 #98 - Drag and drop functionality added 2021-09-10 13:04:49 +02:00
Elio Struyf
eca6e00bf2 New images 2021-09-10 09:42:04 +02:00
Elio Struyf
afcccd780b New media images 2021-09-10 09:37:06 +02:00
Elio Struyf
239c611d3e Add bg to media card 2021-09-10 09:31:46 +02:00
Elio Struyf
4a2f362ed7 Version number update 2021-09-10 09:10:42 +02:00
Elio
b040669bfb Added Windows path media parsing 2021-09-09 19:44:10 +02:00
Elio
2c5a35d9b8 Windows fixes 2021-09-09 19:27:54 +02:00
Elio Struyf
eade3f4799 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2021-09-09 18:36:51 +02:00
Elio Struyf
98d8daaa7c Run cleanup 2021-09-09 18:36:43 +02:00
Elio
1c227c547f Always show unregister folder action, due to new pageFolders setting 2021-09-09 18:34:18 +02:00
Elio Struyf
167c1fafa1 Small header fix + webpack dev mode 2021-09-09 17:54:37 +02:00
Elio Struyf
edf5fc8401 Added beta readme 2021-09-09 14:22:34 +02:00
Elio Struyf
3de0006651 Added .all-contributorsrc 2021-09-09 12:14:17 +02:00
Elio Struyf
f79d382da5 Lightbox + documentation updates 2021-09-09 10:02:51 +02:00
Elio Struyf
11bb0fabcd Update URL 2021-09-09 08:07:59 +02:00
Elio Struyf
4c43eb5eeb Updated beta baseImagesUrl 2021-09-09 07:17:39 +02:00
Elio Struyf
364d6ec3f9 Updated dependency 2021-09-08 16:34:28 +02:00
Elio Struyf
40646fd1b3 Added media refresh + spinner 2021-09-08 16:18:26 +02:00
Elio Struyf
2769eb9b71 Fix beta startup + main release script to reset the id 2021-09-08 10:29:34 +02:00
Elio Struyf
aa97051437 Updated beta script 2021-09-08 10:14:36 +02:00
Elio Struyf
496391b048 Updated changelog + beta release script 2021-09-08 10:12:15 +02:00
Elio Struyf
2e743c896f Stable version check 2021-09-08 10:05:05 +02:00
Elio Struyf
3941c33b1d Updated isProduction function 2021-09-08 10:01:36 +02:00
Elio Struyf
cdc49c09f1 Link to beta docs 2021-09-08 09:52:39 +02:00
Elio Struyf
fc7300ba87 Beta doc updates 2021-09-08 09:43:11 +02:00
Elio Struyf
d3aa51c9ed Updated docs 2021-09-08 09:09:24 +02:00
Elio Struyf
387c906af2 Update docs 2021-09-08 09:06:07 +02:00
Elio Struyf
d04c2a96b7 Updated beta release 2021-09-08 08:51:00 +02:00
Elio Struyf
797c4cd2b1 Updated beta number 2021-09-08 08:46:35 +02:00
Elio Struyf
0ebc874796 3.1.0 2021-09-08 08:43:33 +02:00
Elio Struyf
0b970a73b6 Updated beta workflow 2021-09-08 08:43:30 +02:00
Elio Struyf
065b2fef1b Update beta script 2021-09-08 08:41:15 +02:00
Elio Struyf
b5033f6b45 Beta release 2021-09-08 08:39:24 +02:00
Elio Struyf
274b341648 Merge pull request #86 from estruyf/dependabot/npm_and_yarn/docs/next-11.1.1
Bump next from 11.1.0 to 11.1.1 in /docs
2021-09-08 08:11:05 +02:00
Elio Struyf
c51f2a80c3 Merge pull request #92 from estruyf/dependabot/npm_and_yarn/docs/remark-html-14.0.1
Bump remark-html from 14.0.0 to 14.0.1 in /docs
2021-09-08 08:10:55 +02:00
dependabot[bot]
e6415a3ee9 Bump remark-html from 14.0.0 to 14.0.1 in /docs
Bumps [remark-html](https://github.com/remarkjs/remark-html) from 14.0.0 to 14.0.1.
- [Release notes](https://github.com/remarkjs/remark-html/releases)
- [Commits](https://github.com/remarkjs/remark-html/compare/14.0.0...14.0.1)

---
updated-dependencies:
- dependency-name: remark-html
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-07 23:30:28 +00:00
Elio Struyf
134a5aaa0d #72 #91 - Further implementation of media view + content folder support 2021-09-07 17:52:34 +02:00
Elio Struyf
86c609f49f media dashboard implementation 2021-09-06 16:26:13 +02:00
Elio Struyf
bf3dec46bc Updated changelog 2021-09-06 10:38:47 +02:00
Elio Struyf
2d7c9cfc05 vscode helper integration 2021-09-06 10:33:40 +02:00
Elio Struyf
7e705ccfd8 #89 - Clear filters, sorting, and grouping button 2021-09-06 10:26:15 +02:00
Elio Struyf
1680637235 Create codeql-analysis.yml 2021-09-06 08:57:01 +02:00
Elio Struyf
3de56b94c3 Fix sitemap 2021-09-06 08:32:16 +02:00
dependabot[bot]
1b5840b2bf Bump next from 11.1.0 to 11.1.1 in /docs
Bumps [next](https://github.com/vercel/next.js) from 11.1.0 to 11.1.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v11.1.0...v11.1.1)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 06:30:20 +00:00
Elio Struyf
b14c4c50cf Added sitemap building 2021-09-06 08:29:14 +02:00
Elio Struyf
1f8c2f5b1e Updated references 2021-09-04 15:01:37 +02:00
Elio Struyf
db3a584ab9 #73 - Color enhancements 2021-09-04 12:57:00 +02:00
Elio Struyf
918f914c91 #73 - persist view type setting 2021-09-04 12:40:28 +02:00
Elio Struyf
9f5d180447 Update changelog 2021-09-03 17:47:39 +02:00
Elio Struyf
77e2c68810 Update changelog 2021-09-03 17:46:59 +02:00
Elio Struyf
60caf743c6 #90 #89 - Recoil implementation + Search sorting fix 2021-09-03 17:46:18 +02:00
Elio Struyf
b8dee7a5c9 #73 - List view 2021-09-03 16:45:22 +02:00
Elio Struyf
3b6a7e9b71 Add CMS to the images 2021-09-03 15:37:50 +02:00
Elio Struyf
648ee34171 Style improvements for smaller screens 2021-09-03 11:36:53 +02:00
Elio Struyf
4a88e471f6 Updated changelog + added links to settings in config settings 2021-09-03 10:56:50 +02:00
Elio Struyf
14a2b715e7 Merge branch 'issue/81' 2021-09-03 10:42:12 +02:00
Elio Struyf
0c368dbe65 #85 - Updated showcases 2021-09-03 10:34:11 +02:00
Elio Struyf
f0b7542d94 Removed unused references 2021-09-03 10:29:23 +02:00
Elio Struyf
80cd7d3eac Updated changelog 2021-09-02 17:27:37 +02:00
Elio Struyf
ef08e0a34b #87 - Fix issue with autofocus and command palette 2021-09-02 17:26:40 +02:00
Elio Struyf
62a2c22ba1 Typo fix 2021-09-02 14:12:53 +02:00
Elio Struyf
1417575b98 Added process variables 2021-09-02 14:09:52 +02:00
Elio Struyf
4862810035 Search implementation 2021-09-02 13:47:27 +02:00
Elio Struyf
e1b9bad05b Mobile navigation 2021-09-02 10:58:04 +02:00
Elio Struyf
47f7521e93 Remove reference 2021-09-02 10:29:15 +02:00
Elio Struyf
771f86710f Website content improvements 2021-09-02 10:26:04 +02:00
Elio Struyf
dc55cedb0d Update Navigation.tsx 2021-09-01 21:21:27 +02:00
Elio Struyf
e93c6832e4 Update CTA.tsx 2021-09-01 21:17:16 +02:00
Elio Struyf
0f0c7f9f3d Update en.ts 2021-09-01 21:14:06 +02:00
Elio Struyf
d9984ce292 #81 - Optimize the content folder settings + migration script for deprecated setting 2021-09-01 18:55:01 +02:00
Elio Struyf
c9e813effe Added Vercel as sponsor 2021-09-01 11:53:59 +02:00
Elio Struyf
3fea99ef24 Updated height 2021-09-01 11:43:27 +02:00
Elio Struyf
06a3841dd3 Added Vercel as sponsor 2021-09-01 11:42:59 +02:00
Elio Struyf
32fb742de5 Updated changelog + added visitors on docs 2021-09-01 09:56:39 +02:00
Elio Struyf
c41b7adcfd updated changelog 2021-09-01 09:49:58 +02:00
Elio Struyf
be55a2237d #77 - Grouping implementation 2021-09-01 09:44:15 +02:00
Elio Struyf
265f788f98 Restructuring 2021-09-01 08:46:33 +02:00
Elio Struyf
c7d3a7dcf6 Updated showcase 2021-08-31 17:44:30 +02:00
Elio Struyf
e02a766070 Updated readme with contributors 2021-08-31 14:31:31 +02:00
Elio Struyf
c9dd684451 Added review 2021-08-31 13:34:51 +02:00
Elio Struyf
5329033b46 Update typos 2021-08-31 12:25:44 +02:00
Elio Struyf
4bd914fdba Styling updates 2021-08-31 12:18:06 +02:00
Elio Struyf
b7f093d55b Updated tsconfig to exclude docs + ignore list 2021-08-31 12:09:56 +02:00
Elio Struyf
87273d9eba updated readme 2021-08-31 11:58:40 +02:00
Elio Struyf
bb1ea17d95 Documentation fixes 2021-08-31 11:55:08 +02:00
Elio Struyf
a554705cab 3.0.2 2021-08-31 11:52:45 +02:00
Elio Struyf
6c9abc572a Update changelog 2021-08-31 11:52:24 +02:00
Elio Struyf
be168b8963 Image optimization 2021-08-31 11:51:45 +02:00
Elio Struyf
d05d6ac95e Merge branch 'docs' 2021-08-31 11:49:54 +02:00
Elio Struyf
700a546b39 #82 - Hide commands 2021-08-31 11:49:35 +02:00
Elio Struyf
0fd6e1c87b Documenation updates 2021-08-31 11:49:14 +02:00
Elio Struyf
733e747f58 Merge branch 'main' into docs 2021-08-30 21:44:39 +02:00
Elio Struyf
672abbea2d Docs 2021-08-30 21:44:16 +02:00
Elio Struyf
b04fdae043 Added link for documentation 2021-08-30 18:10:14 +02:00
Elio Struyf
e7a124f178 Mobile fixes 2021-08-30 18:01:31 +02:00
Elio Struyf
4994761fe2 empty folder 2021-08-30 15:33:58 +02:00
Elio Struyf
ad4cbd748b Added web dependency 2021-08-30 15:31:58 +02:00
Elio Struyf
3d85902d2c Update documentation 2021-08-30 15:26:06 +02:00
Elio Struyf
8b558fa2f7 Merge branch 'docs' 2021-08-30 15:14:24 +02:00
Elio Struyf
94a8610edf Docs site added 2021-08-30 15:13:06 +02:00
Elio Struyf
ad2a30b5f8 Update issue templates 2021-08-30 10:14:53 +02:00
Elio Struyf
39d3cf46f3 Merge branch 'main' into docs 2021-08-30 09:45:53 +02:00
Elio Struyf
596804ac79 Docs 2021-08-30 09:44:48 +02:00
Elio Struyf
6ac15e9252 3.0.1 2021-08-30 08:29:27 +02:00
Elio Struyf
1a8f10e942 Updated changelog 2021-08-30 08:29:14 +02:00
Elio Struyf
779c1ef6ae #79 - scrollbar fix 2021-08-30 08:28:24 +02:00
Elio Struyf
600eb150bf Teal icon 2021-08-28 09:37:37 +02:00
Elio Struyf
0ef0fbc3f9 3.0.0 2021-08-27 12:32:09 +02:00
Elio Struyf
95ae50c24e Small optimization for content folder selection 2021-08-27 12:31:50 +02:00
Elio Struyf
3a43c4b5b9 Windows fixes 2021-08-27 12:21:11 +02:00
Elio Struyf
0e73c9bec5 Change order 2021-08-27 11:36:03 +02:00
Elio Struyf
9479f5d53d Updated readme 2021-08-27 11:35:21 +02:00
Elio Struyf
7716d98868 #69 - Welcome screen integration 2021-08-27 11:30:25 +02:00
Elio Struyf
918a33b45e Welcome message updated 2021-08-26 20:40:30 +02:00
Elio Struyf
2c1fd2890c Updates to support the upcoming welcome message 2021-08-26 20:28:43 +02:00
Elio Struyf
939cf94de6 Merge branch 'issue/65' into main 2021-08-26 20:02:08 +02:00
Elio Struyf
a912a7702d Make the dashboard start first 2021-08-26 14:05:59 +02:00
Elio Struyf
b35b0e9515 Added publicFolder info 2021-08-26 13:43:43 +02:00
Elio Struyf
8ad93a6916 Updated readme 2021-08-26 13:36:15 +02:00
Elio Struyf
aac34c96f7 Added sponsor message ❤️ 2021-08-26 13:21:00 +02:00
Elio Struyf
975bb10001 Support filtering, documentation updates 2021-08-26 11:40:18 +02:00
Elio Struyf
6cce35de6c Dashboard updates to support content creation + search 2021-08-25 21:39:26 +02:00
Elio Struyf
722c0d6888 #65 - Dashboard implementation 2021-08-24 21:01:58 +02:00
Elio Struyf
ce8a343ffd Merge branch 'poc-tailwind' 2021-08-24 12:06:44 +02:00
Elio Struyf
5e539a3d14 #64 - Toggle implementation 2021-08-24 10:30:23 +02:00
Elio Struyf
1224316217 Refactoring + optimizing files and folder updates 2021-08-23 21:27:47 +02:00
Elio Struyf
8d7aeb3edd #61 - Implementation of recently modified files 2021-08-23 15:37:51 +02:00
Elio Struyf
c65500b061 Tailwind css 2021-08-23 13:49:01 +02:00
Elio Struyf
8642b8b46a FileList component 2021-08-23 13:48:41 +02:00
Elio Struyf
ea381eac51 Open file updates 2021-08-23 13:22:47 +02:00
Elio Struyf
6395e471f0 2.5.1 2021-08-23 08:51:09 +02:00
Elio Struyf
59f0351b79 Fix typo 2021-08-23 08:50:59 +02:00
Elio Struyf
1b16fa69d0 Update docs 2021-08-21 10:23:54 +02:00
Elio Struyf
e8f67590bc Updated image 2021-08-19 14:22:22 +02:00
Elio Struyf
b34a8feb63 changelog update 2021-08-19 14:09:02 +02:00
Elio Struyf
fcbca1e48a Fix typo 2021-08-19 14:08:44 +02:00
Elio Struyf
4585790b45 updated changelog 2021-08-19 14:08:00 +02:00
Elio Struyf
445b64a2ec 2.5.0 2021-08-19 14:07:47 +02:00
Elio Struyf
5d18d09b72 #60 - site preview + refactoring 2021-08-19 13:34:15 +02:00
Elio Struyf
fdfdfda6ce 2.4.1 2021-08-16 14:30:18 +02:00
Elio Struyf
f016ae27ec Better highlight 2021-08-16 14:30:10 +02:00
233 changed files with 8345 additions and 1238 deletions

11
.all-contributorsrc Normal file
View File

@@ -0,0 +1,11 @@
{
"files": [],
"imageSize": 100,
"contributorsPerLine": 7,
"contributorsSortAlphabetically": false,
"badgeTemplate": "",
"contributorTemplate": "",
"types": {},
"skipCi": "true",
"contributors": []
}

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: 'Issue: '
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: 'Enhancement: '
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

20
.github/ISSUE_TEMPLATE/showcase.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Showcase
about: Let us know that you are using Front Matter and we'll add you on our showcase page
title: 'Showcase: '
labels: 'showcase'
assignees: ''
---
**Title you want to give your site**
Define a clear title that will be used on the showcase page. Example: `Front Matter`.
**Link to the site**
A URL to the site to add.
**A nice and clean description**
Keep it simple. Just let us know which static-site generator you used, and other frameworks.

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '24 14 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

27
.github/workflows/release-beta.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: BETA Release
on:
push:
branches:
- dev
jobs:
build:
name: "Build and release"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- name: Install the dependencies
run: npm i
- name: Prepare BETA
run: node scripts/beta-release.js $GITHUB_RUN_ID
- name: Publish
run: npx vsce publish -p ${{ secrets.VSCE_PAT }} --baseImagesUrl https://raw.githubusercontent.com/estruyf/vscode-front-matter/dev

View File

@@ -19,10 +19,10 @@ jobs:
- name: Install the dependencies
run: npm i
- name: Install vsce
run: npm i -g vsce
- name: Prepare MAIN release
run: node scripts/main-release.js
- name: Publish
run: vsce publish -p ${{ secrets.VSCE_PAT }}
run: npx vsce publish -p ${{ secrets.VSCE_PAT }}

View File

@@ -0,0 +1,9 @@
---
title:
slug:
description:
date: 2019-08-22T15:20:28.000Z
lastmod: 2019-08-22T15:20:28.000Z
weight: 1
type: documentation
---

66
.vscode/recoil.code-snippets vendored Normal file
View File

@@ -0,0 +1,66 @@
{
"Recoil Atom": {
"prefix": "sq-atom",
"body": [
"import { atom } from 'recoil';",
"",
"export const ${1:CollectionId}Atom = atom({",
" key: '${1:CollectionId}Atom',",
" default: 1",
"});"
],
"description": "Creates a new atom",
"scope": "typescript"
},
"Recoil Selector (sync)": {
"prefix": "sq-selector-sync",
"body": [
"import { selector } from 'recoil';",
"",
"export const ${1:CollectionData}Selector = selector({",
" key: '${1:CollectionData}Selector',",
" get: ({get}) => {",
" return get(${1:CollectionData}Atom);",
" }",
"});"
],
"description": "Creates a new synchronous selector",
"scope": "typescript"
},
"Recoil Selector (async)": {
"prefix": "sq-selector-async",
"body": [
"import { selector } from 'recoil';",
"",
"export const ${1:CollectionData}Selector = selector({",
" key: '${1:CollectionData}Selector',",
" get: async ({get}) => {",
" return await dataFetch(get(${2:CollectionIdState}));",
" }",
"});"
],
"description": "Creates a new asynchronous selector",
"scope": "typescript"
},
"Recoil selectorFamily": {
"prefix": "sq-selector-fam",
"body": [
"import { selectorFamily } from 'recoil';",
"",
"export const ${1:CollectionData}Selector = selectorFamily({",
" key: '${1:CollectionData}Selector',",
" get: id => async () => {",
" return await dataFetch({id});",
" }",
"});"
],
"description": "Creates a selectorFamily (same as selector, but used to provide parameters)",
"scope": "typescript"
},
"useTranslation": {
"prefix": ["sq-translation", "useTranslation"],
"body": "const { t: strings } = useTranslation();",
"description": "Include the translations",
"scope": "typescriptreact"
}
}

View File

@@ -7,5 +7,11 @@
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
"typescript.tsc.autoDetect": "off",
"eliostruyf.writingstyleguide.terms.isDisabled": true,
"eliostruyf.writingstyleguide.biasFree.isDisabled": true,
"exportall.config.folderListener": [
"/src/pagesView/state/atom",
"/src/pagesView/state/selectors"
]
}

View File

@@ -10,3 +10,11 @@ vsc-extension-quickstart.md
**/*.ts
webpack.config.js
node_modules
docs
tailwind.config.js
sample
postcss.config.js
.templates
.github
scripts
.all-contributorsrc

View File

@@ -1,5 +1,62 @@
# Change Log
## [4.0.0] - 2021-09-22 - [Release Notes](https://beta.frontmatter.codes/updates/v4_0_0)
- [#101](https://github.com/estruyf/vscode-front-matter/issues/101): Date picker available on the metadata section
- [#102](https://github.com/estruyf/vscode-front-matter/issues/102): Support comma separated arrays in front matter
- [#103](https://github.com/estruyf/vscode-front-matter/issues/103): Added title and description field to the metadata section
- [#104](https://github.com/estruyf/vscode-front-matter/issues/104): Allow to set images in front matter from the metadata panel section
- [#105](https://github.com/estruyf/vscode-front-matter/issues/105): Content Type support with backwards compatibility
- [#106](https://github.com/estruyf/vscode-front-matter/issues/106): Introduction of team level settings for Front Matter
- [#107](https://github.com/estruyf/vscode-front-matter/issues/107): Number field support added in content type fields
- [#108](https://github.com/estruyf/vscode-front-matter/issues/108): Choice field support added in content type fields
- [#109](https://github.com/estruyf/vscode-front-matter/issues/109): JSON Config script added to automate the JSON schema
- [#111](https://github.com/estruyf/vscode-front-matter/issues/111): Insert media into the markdown contents
- [#112](https://github.com/estruyf/vscode-front-matter/issues/112): Add snippet support for media insertion
## [3.1.0] - 2021-09-10
- BETA version available at: [beta.frontmatter.codes](https://beta.frontmatter.codes)
- [#72](https://github.com/estruyf/vscode-front-matter/issues/72): Media view on the dashboard
- [#73](https://github.com/estruyf/vscode-front-matter/issues/73): List view option for the dashboard
- [#77](https://github.com/estruyf/vscode-front-matter/issues/77): Dashboard grouping pages functionality integrated
- [#81](https://github.com/estruyf/vscode-front-matter/issues/81): Optimizing the content folders to use a new setting to simplify configuration
- [#87](https://github.com/estruyf/vscode-front-matter/issues/87): Fix issue with autofocus and command palette
- [#88](https://github.com/estruyf/vscode-front-matter/issues/88): Fix issue with search sorting
- [#89](https://github.com/estruyf/vscode-front-matter/issues/89): Clear filter, sorting, and grouping button added
- [#90](https://github.com/estruyf/vscode-front-matter/issues/90): Refactoring to use Recoil state management
- [#91](https://github.com/estruyf/vscode-front-matter/issues/91): Support image previews from content folders
- [#98](https://github.com/estruyf/vscode-front-matter/issues/98): Add drag and drop support for media upload in a folder
## [3.0.2] - 2021-08-31
- [#82](https://github.com/estruyf/vscode-front-matter/issues/82): Hide the register and unregister commands from the command palette
## [3.0.1] - 2021-08-30
- [#79](https://github.com/estruyf/vscode-front-matter/issues/79): Fix scrollbar not visible on the welcome screen
## [3.0.0] - 2021-08-27
- [#61](https://github.com/estruyf/vscode-front-matter/issues/61): List of recently modified files
- [#64](https://github.com/estruyf/vscode-front-matter/issues/64): Publish toggle for easier publishing an article
- [#65](https://github.com/estruyf/vscode-front-matter/issues/65): Aggregate articles in draft
- [#66](https://github.com/estruyf/vscode-front-matter/issues/66): New dashboard webview on which you can manage all your content
- [#69](https://github.com/estruyf/vscode-front-matter/issues/69): Welcome screen for getting started
## [2.5.1] - 2020-08-23
- Fix typo in the `package.json` file for the preview command
## [2.5.0] - 2020-08-19
- Moved the center layout button to the other actions section
- [#60](https://github.com/estruyf/vscode-front-matter/issues/60): Added the ability to open a site preview in VS Code
## [2.4.1] - 2020-08-16
- Better editor highlighting functionality
## [2.4.0] - 2020-08-16
- [#21](https://github.com/estruyf/vscode-front-matter/issues/21): Folding provider for Front Matter implemented

123
README.beta.md Normal file
View File

@@ -0,0 +1,123 @@
<h1 align="center">
<a href="https://beta.frontmatter.codes">
<img alt="Front Matter BETA" src="./assets/frontmatter-beta.png">
</a>
</h1>
<h2 align="center">Front Matter a CMS running straight in Visual Studio Code</h2>
<h2 align="center">This is the BETA version of Front Matter. If you were looking for the main version, check it out at <a href="https://frontmatter.codes">frontmatter.codes</a></h2>
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter" title="Check it out on the Visual Studio Marketplace">
<img src="https://vsmarketplacebadge.apphb.com/version/eliostruyf.vscode-front-matter.svg" alt="Visual Studio Marketplace" style="display: inline-block" />
</a>
<img src="https://vsmarketplacebadge.apphb.com/installs/eliostruyf.vscode-front-matter.svg" alt="Number of installs" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadge.apphb.com/rating/eliostruyf.vscode-front-matter.svg" alt="Ratings" style="display: inline-block;margin-left:10px" />
<a href="https://www.buymeacoffee.com/zMeFRy9" title="Buy me a coffee" style="margin-left:10px">
<img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-€%203-blue?logo=buy-me-a-coffee&style=flat" alt="Buy me a coffee" style="display: inline-block" />
</a>
</p>
<h2 align="center">
<a href="https://beta.frontmatter.codes" title="Documentation @ beta.frontmatter.codes">
Check out the extension documentation at beta.frontmatter.codes
</a>
</h2>
![](./assets/v4.0.0/banner-v2.png)
## What is Front Matter?
Front Matter BETA is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
Our main extension features are:
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
- Site preview within Visual Studio Code
- SEO checks for title, description, and keywords
- Support for custom actions/scripts
- and many more
<p align="center">
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
</p>
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 4**
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
**Version 3**
In version v3 we introduced the welcome and dashboard webview. The welcome view allows to get you started using the extension, and the dashboard allows you to manage all your markdown pages in one place. This makes it easy to search, filter, sort, and more.
**Version 2**
In version v2 we released the re-designed sidebar panel with improved SEO support. This extension makes it the only extension to manage your Markdown pages for your static sites in Visual Studio Code.
<p align="center" style="margin-top: 2rem;">
<a href="https://www.producthunt.com/posts/front-matter?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-front-matter" target="_blank">
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.png?post_id=309033&theme=dark" alt="Front Matter BETA - Managing your static sites straight from within VS Code | Product Hunt" style="width: 250px; height: 40px;" />
</a>
</p>
## Installation
You can get the extension via:
- The VS Code marketplace: [VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter).
- The extension CLI: `ext install eliostruyf.vscode-front-matter`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open extension in VS Code</a>
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
### Beta version
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
## Documentation
<h2 align="center">
<a href="https://beta.frontmatter.codes" title="Documentation @ beta.frontmatter.codes">
Check out the extension documentation at beta.frontmatter.codes
</a>
</h2>
## 👉 Contributors 🤘
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
</a>
<br />
<br />
## 🖤 Sponsors
<p align="center">
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
<img src="assets/sponsors/powered-by-vercel.png" />
</a>
</p>
<br />
<br />
<p align="center">
<a href="https://visitorbadge.io">
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
</a>
</p>

464
README.md
View File

@@ -1,10 +1,10 @@
<h1 align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter">
<img alt="Front Matter" src="./assets/front-matter.png">
<a href="https://frontmatter.codes">
<img alt="Front Matter" src="./assets/frontmatter-teal-128x128.png">
</a>
</h1>
<h2 align="center">Front Matter is an essential Visual Studio Code extension when you want to manage the markdown pages of your static sites.</h2>
<h2 align="center">Front Matter a CMS running straight in Visual Studio Code</h2>
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter" title="Check it out on the Visual Studio Marketplace">
@@ -20,396 +20,102 @@
</a>
</p>
This VSCode extension simplifies working with your markdown articles' front matter when using a static site generator like Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more... For example, you can keep a list of used tags, categories and add/remove them from your article with the extension.
<h2 align="center">
<a href="https://frontmatter.codes" title="Documentation @ frontmatter.codes">
Check out the extension documentation at frontmatter.codes
</a>
</h2>
The extension will automatically verify if your title and description are SEO compliant. If this would not be the case, it will give you a warning.
![](./assets/v4.0.0/banner-v2.png)
## What is Front Matter?
Front Matter is an essential Visual Studio Code extension that simplifies working and managing your markdown articles. We created the extension to support many static-site generators like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
The extension brings Content Management System (CMS) capabilities straight within Visual Studio Code. For example, you can keep a list of the used tags, categories, create content, and so much more.
Our main extension features are:
- Page dashboard where you can get an overview of all your markdown pages. You can use it to search, filter, sort your contents.
- Site preview within Visual Studio Code
- SEO checks for title, description, and keywords
- Support for custom actions/scripts
- and many more
<p align="center">
<img src="./assets/v4.0.0/preview.png" alt="Site preview" style="display: inline-block" />
</p>
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 4**
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
**Version 3**
In version v3 we introduced the welcome and dashboard webview. The welcome view allows to get you started using the extension, and the dashboard allows you to manage all your markdown pages in one place. This makes it easy to search, filter, sort, and more.
**Version 2**
In version v2.0.0 we released the newly redesigned sidebar panel with improved SEO support. This extension makes it the only extension to manage your Markdown pages for your static sites in Visual Studio Code.
In version v2 we released the re-designed sidebar panel with improved SEO support. This extension makes it the only extension to manage your Markdown pages for your static sites in Visual Studio Code.
<h2 id="table-of-contents">Table of Contents</h2>
<details open="open">
<summary>Table of Contents</summary>
<ol>
<li><a href="#markdown-features">Markdown features</a></li>
<li><a href="#the-panel">The panel</a></li>
<li><a href="#custom-actions">Custom actions</a></li>
<li><a href="#creating-articles-from-templates">Create articles from templates</a></li>
<li><a href="#syntax-highlighting-for-hugo-shortcodes">Syntax highlighting for Hugo Shortcodes</a></li>
<li><a href="#available-commands">Available commands</a></li>
<li><a href="#extension-settings">Extension settings</a></li>
<li><a href="#feedback--issues--ideas">Feedback / issues / ideas</a></li>
</ol>
</details>
## Markdown features
The Front Matter extension tries to make it easy to manage your Markdown pages/content. Within a Markdown page, we allow you to fold the file's Front Matter to be less distracting when writing. Also, do we highlight the Front Matter content to create a visual difference between content and metadata.
### Front Matter folding
<p align="center">
<img src="./assets/v2.4.0/folding.png" alt="Front Matter folding" style="display: inline-block" />
<p align="center" style="margin-top: 2rem;">
<a href="https://www.producthunt.com/posts/front-matter?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-front-matter" target="_blank">
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.png?post_id=309033&theme=dark" alt="Front Matter - Managing your static sites straight from within VS Code | Product Hunt" style="width: 250px; height: 40px;" />
</a>
</p>
### Front Matter highlighting
## Installation
You can get the extension via:
- The VS Code marketplace: [VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter).
- The extension CLI: `ext install eliostruyf.vscode-front-matter`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open extension in VS Code</a>
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
### Beta version
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
- The VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
## Documentation
<h2 align="center">
<a href="https://frontmatter.codes" title="Documentation @ frontmatter.codes">
Check out the extension documentation at frontmatter.codes
</a>
</h2>
## 👉 Contributors 🤘
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
</a>
<br />
<br />
## 🖤 Sponsors
<p align="center">
<img src="./assets/v2.4.0/fm-highlight.png" alt="Front Matter highlighting" style="display: inline-block" />
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
<img src="assets/sponsors/powered-by-vercel.png" />
</a>
</p>
> **Info**: If you do not want this feature, you can disable it in the extension settings -> `Highlight Front Matter` or by setting the `frontMatter.content.fmHighlight` setting to `false`.
## The panel
The Front Matter panel allows you to perform most of the extension actions by just a click on the button and it shows the SEO statuses of your title, description, and more.
Initially, this panel has been created to make it easier to add tags and categories to your articles as the current VSCode multi-select is not optimal to use.
To leverage most of the capabilities of the extension. SEO information and everyday actions like slug optimization, updating the date, and publish/drafting the article.
When you open the panel and the current file is not a Markdown file, it will contain the following sections:
<br />
<br />
<p align="center">
<img src="./assets/v2.4.0/baseview.png" alt="Base view" style="display: inline-block" />
</p>
> **Info**: both **Global Settings** and **Other Actions** sections are shown for the base view as when a Markdown file is openend.
When you open the Front Matter panel on a Markdown file, you get to see the following sections:
**Global Settings**
<p align="center">
<img src="./assets/v2.4.0/global-settings.png" alt="Global settings" style="display: inline-block" />
</p>
**SEO Status**
<p align="center">
<img src="./assets/v2.0.0/seo.png" alt="SEO article status" style="display: inline-block" />
</p>
**Actions**
<p align="center">
<img src="./assets/v2.4.0/actions.png" alt="Actions" style="display: inline-block" />
</p>
**Metadata: Keywords, Tags, Categories**
<p align="center">
<img src="./assets/v2.0.0/metadata.png" alt="Article metadata" style="display: inline-block" />
</p>
> **Info**: By default, the tags/categories picker allows you to insert existing and none tags/categories. When you enter a none existing tag/category, the panel shows an add `+` icon in front of that button. This functionality allows you to store this tag/category in your settings. If you want to disable this feature, you can do that by setting the `frontMatter.panel.freeform` setting to `false`.
**Other actions**
<p align="center">
<img src="./assets/v2.4.0/other-actions.png" alt="Other actions" style="display: inline-block" />
</p>
**Info**: The `Enable write settings` action allow you to make Markdown specific changes to optimize the writing of your articles. It will change settings like the `fontSize`, `lineHeight`, `wordWrap`, `lineNumbers` and more.
## Custom actions
Since version `1.15.0`, the extension allows you to create your own custom actions, by running Node.js scripts from your project. In order to use this functionality, you will need to configure the [`frontMatter.custom.scripts`](#frontmattercustomscripts) setting for your project.
Once a custom action has been configured, it will appear on the Front Matter panel.
<p align="center">
<img src="./assets/v2.0.0/custom-action.png" alt="Custom action" style="display: inline-block" />
</p>
The current workspace-, file-path, and front matter data will be passed as an argument. In your script fetch these arguments as follows:
```javascript
const arguments = process.argv;
if (arguments && arguments.length > 0) {
const workspaceArg = arguments[2]; // The workspace path
const fileArg = arguments[3]; // The file path
const frontMatterArg = arguments[4]; // Front matter data
console.log(`The content returned for your notification.`);
}
```
> A sample file can be found here: [script-sample.js](./sample/script-sample.js)
The output of the script will be passed as a notification, and it allows you to copy the output.
<p align="center">
<img src="./assets/custom-action-notification.png" alt="Custom action notification" style="display: inline-block" />
</p>
## Creating articles from templates
By default, the extension looks for files stored in a `.templates` folder that should be located in your website project's root.
> **Info**: You can overwrite the path by specifying it with the `frontMatter.templates.folder` setting.
When adding files in the folder, you'll be able to run the `Front Matter: New article from template` from a command or explorer menu. It will present you with the article template options once you pick one and specify the title. It creates the file and updates its front matter.
> **Info**: By default, the extension will create articles with a `yyyy-MM-dd` prefix. If you do not want that or change the date format, you can update the `frontMatter.templates.prefix` setting.
## Syntax highlighting for Hugo Shortcodes
<p align="center">
<img src="./assets/syntax-highlighting.png" alt="Shortcode syntax highlighting" style="display: inline-block" />
</p>
## Available commands
**Front Matter: Initialize project**
This command will initialize the project with a template folder and an article template. It makes it easier to get you started with the extension and creating your content.
**Front Matter: Create a template from current file**
This command allows you to create a new template from the current open Markdown file. It will ask you for the name of the template and if you want to keep the current file its content in the template.
> **Info**: The create as template action is also available from the `other actions` section in the Front Matter panel.
**Front Matter: New article from template**
With this command, you can easily create content in your project within the registered folders and provided templates.
You can register and unregister folders by right-clicking on the folder in your VSCode explorer panel.
<p align="center">
<img src="./assets/v2.1.0/register-folder.png" alt="Register/unregister a folder" style="display: inline-block" />
</p>
Once you registered a folder and a template has been defined ([how to create a template](#creating-articles-from-templates)), you can make use of this command.
> **Info**: The benefit of this command is that you do not need to search the folder in which you want to create a new article/page/... The extension will do it automatically for you.
**Front Matter: Create <tag | category>**
Creates a new <tag | category> and allows you to include it into your post automatically
<p align="center">
<img src="./assets/create-tag-category.gif" alt="Create tag or category" style="display: inline-block" />
</p>
**Front Matter: Insert <tags | categories>**
Inserts a selected <tags | categories> into the front matter of your article/post/... - When using this command, the Front Matter panel opens and focuses on the specified type.
> **Info**: This experience changed in version `1.11.0`.
**Front Matter: Export all tags & categories to your settings**
Export all the already used tags & categories in your articles/posts/... to your user settings.
**Front Matter: Remap or remove tag/category in all articles**
This command helps you quickly update/remap or delete a tag or category in your markdown files. The extension will ask you to select the taxonomy type (*tag* or *category*), the old taxonomy value, and the new one (leave the input field *blank* to remove the tag/category).
> **Info**: Once the remapping/deleting process completes. Your VSCode settings update with all new taxonomy tags/categories.
**Front Matter: Set current date**
Update the `date` property of the current article/post/... to the current date & time.
**Optional**: if you want, you can specify the date property format by adding your settings' preference. Settings key: `frontMatter.taxonomy.dateFormat`. Check [date-fns formatting](https://date-fns.org/v2.0.1/docs/format) for more information on which patterns you can use.
**Front Matter: Set lastmod date**
Update the `lastmod` (last modified) property of the current article/post/... to the current date & time. By setting the `frontMatter.content.autoUpdateDate` setting, it can be done automatically when performing changes to your markdown files.
> **note**: Uses the same date format settings key as current date: `frontMatter.taxonomy.dateFormat`.
**Front Matter: Generate slug based on article title**
This command generates a clean slug for your article. It removes known stop words, punctuations, and special characters.
Example:
```
title: Just a sample page with a title
slug: sample-page-title
```
You can also specify a prefix and suffix, which can be added to the slug if you want. Use the following settings to do this: `frontMatter.taxonomy.slugPrefix` and `frontMatter.taxonomy.slugSuffix`. By default, both options are not provided and will not add anything to the slug. Another setting is to allow you to sync the filename with the generated slug. The setting you need to turn on enable for this is `frontMatter.taxonomy.alignFilename`.
> **Info**: At the moment, the extension only supports English stopwords.
### Usage
- Start by opening the command prompt:
- Windows: ⇧+ctrl+P
- Mac: ⇧+⌘+P
- Use one of the commands from above
## Where is the data stored?
The tags and categories are stored in the project VSCode user settings. You can find them back under: `.vscode/settings.json`.
```json
{
"frontMatter.taxonomy.tags": [],
"frontMatter.taxonomy.categories": []
}
```
## Extension settings
The extension has more settings that allow you to configure it to your needs further. Here is a list of settings that you can set:
### `frontMatter.taxonomy.seoTitleLength`
Specifies the optimal title length for SEO (set to `-1` to turn it off). Default value: `60`.
```json
{
"frontMatter.taxonomy.seoTitleLength": 60
}
```
### `frontMatter.taxonomy.seoDescriptionLength`
Specifies the optimal description length for SEO (set to `-1` to turn it off). Default value: `160`.
```json
{
"frontMatter.taxonomy.seoDescriptionLength": 160
}
```
### `frontMatter.taxonomy.seoContentLength`
Specifies the optimal minimum length for your articles. Between 1,760 words 2,400 is the absolute ideal article length for SEO in 2021. (set to `-1` to turn it off).
```json
{
"frontMatter.taxonomy.seoContentLength": 1760
}
```
### `frontMatter.taxonomy.seoDescriptionLength`
Specifies the name of the SEO description field for your page. Default is `description`.
```json
{
"frontMatter.taxonomy.seoDescriptionField": "description"
}
```
### `frontMatter.taxonomy.frontMatterType`
Specify which Front Matter language you want to use. The extension supports `YAML` (default) and `TOML`.
```json
{
"frontMatter.taxonomy.frontMatterType": "YAML"
}
```
### `frontMatter.taxonomy.indentArrays`
Specify if arrays in the front matter are indented. Default: `true`. If you do not want to indent the array values, you can update it with the following setting change:
```json
{
"frontMatter.taxonomy.indentArrays": false
}
```
### `frontMatter.taxonomy.noPropertyValueQuotes`
Specify the property names of which you want to remove the quotes in the output value. **Warning**: only use this when you know what you are doing. If you're going to, for instance, remove the quotes from the date property, you can add the following:
```json
{
"frontMatter.taxonomy.noPropertyValueQuotes": ["date"]
}
```
### `frontMatter.taxonomy.dateField`
Specifies the date field name to use in your Front Matter. Default value: `date`.
```json
{
"frontMatter.taxonomy.dateField": "date"
}
```
### `frontMatter.taxonomy.modifiedField`
Specifies the modified date field name to use in your Front Matter. Default value: `lastmod`.
```json
{
"frontMatter.taxonomy.modifiedField": "lastmod"
}
```
### `frontMatter.custom.scripts`
Allows you to specify a title and script path (starting relative from the root of your project). These values will be used to create custom actions on the Front Matter panel. Default value: `[]`.
```json
{
"frontMatter.custom.scripts": [{
"title": "Generate social image",
"script": "./scripts/social-img.js",
"nodeBin": "~/.nvm/versions/node/v14.15.5/bin/node"
}]
}
```
> **Important**: When the command execution would fail when it cannot find the `node` command. You are able to specify your path to the node app. This is for instance required when using `nvm`.
### `frontMatter.content.folders`
This array of folders defines where the extension can easily create new content by running the create article command.
```json
{
"frontMatter.content.folders": [{
"title": "Articles",
"fsPath": "<the path to the folder>",
"paths": ["<wsl-folder-path>"]
}]
}
```
> **Important**: This setting can be configured by right-clicking on a folder in the VSCode file explorer view and clicking on the `Front Matter: Register folder` menu item.
### `frontMatter.content.autoUpdateDate`
Specify if you want to automatically update the modification date of your markdown page when doing changes to it. Default: `false`.
```json
{
"frontMatter.content.autoUpdateDate": false
}
```
### `frontMatter.content.fmHighlight`
Specify if you want to highlight the Front Matter in the Markdown file. Default: `true`.
```json
{
"frontMatter.content.fmHighlight": true
}
```
## Feedback / issues / ideas
Please submit them via creating an issue in the project repository: [issue list](https://github.com/estruyf/vscode-front-matter/issues).
<p align="center">
<a href="#">
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" />
<a href="https://visitorbadge.io">
<img src="https://estruyf-github.azurewebsites.net/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
</a>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
assets/frontmatter-beta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFE45E;}
.st1{fill:none;stroke:#FFE45E;stroke-width:2;stroke-miterlimit:10;}
.st2{font-family:'MyriadPro-Bold';}
.st3{font-size:8px;}
</style>
<g>
<g>
<path class="st0" d="M4.1,10.2H2.4V2.1h3.1V4H4.1v1.2h1.2V7H4.1V10.2z"/>
<path class="st0" d="M10.7,10.2H8.9L8,7.3c0-0.1,0-0.1,0-0.2C7.9,7.1,7.9,7,7.8,6.8v0.6v2.9H6.1V2.1h1.8c0.8,0,1.3,0.2,1.8,0.6
c0.5,0.5,0.8,1.2,0.8,2.1c0,1-0.4,1.6-1.1,2L10.7,10.2z M7.9,5.8L7.9,5.8c0.3,0,0.5-0.1,0.6-0.3S8.7,5,8.7,4.8c0-0.6-0.3-1-0.8-1
l0,0V5.8z"/>
<path class="st0" d="M16.1,6.2c0,1.2-0.2,2.3-0.7,3.1s-1.1,1.2-1.7,1.2s-1.2-0.3-1.6-0.9c-0.6-0.8-0.9-1.9-0.9-3.4
s0.3-2.6,0.9-3.4C12.6,2.3,13,2,13.7,2c0.8,0,1.3,0.4,1.8,1.2C15.8,3.8,16.1,4.8,16.1,6.2z M14.3,6.2c0-1.4-0.2-2.2-0.7-2.2
c-0.2,0-0.4,0.2-0.5,0.6c-0.1,0.4-0.2,0.9-0.2,1.6c0,0.7,0.1,1.2,0.2,1.6c0.1,0.4,0.3,0.6,0.5,0.6c0.2,0,0.4-0.2,0.5-0.6
C14.2,7.3,14.3,6.9,14.3,6.2z"/>
<path class="st0" d="M16.8,10.2V2.1h1.7l0.9,2.9c0.1,0.1,0.1,0.3,0.2,0.6c0.1,0.2,0.1,0.5,0.2,0.8L20,7c-0.1-0.7-0.1-1.3-0.2-1.8
s-0.1-1-0.1-1.2V2.1h1.7v8.2h-1.6l-0.9-3c-0.1-0.3-0.2-0.6-0.3-0.9c-0.1-0.3-0.1-0.6-0.2-0.8c0,0.6,0.1,1.1,0.1,1.5
c0,0.4,0,0.8,0,1.2v2.1h-1.7V10.2z"/>
<path class="st0" d="M24.6,10.2h-1.7V4h-1V2.1h3.7V4h-1.1V10.2z"/>
</g>
</g>
<rect class="st1" width="28" height="28"/>
<g>
<g>
<path class="st0" d="M3.1,11.6H4l0.6,3c0.1,0.4,0.2,0.8,0.2,1.2C4.9,16.2,4.9,16.6,5,17c0-0.1,0-0.1,0-0.1v-0.1l0.2-0.9l0.1-0.8
l0.1-0.5l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3c0-0.1,0-0.2,0-0.2v-1v-0.9l0,0c0,0,0,0,0-0.1v0.2c0,0.2,0,0.3-0.1,0.5
c-0.1,0.2,0,0.2-0.1,0.3L6,15.7V16l-0.6,3.3H4.7l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2l-0.3,5.2h-1
L3.1,11.6z"/>
<path class="st0" d="M9.4,11.6h0.8l1.6,7.5h-1l-0.3-1.5H9l-0.3,1.5h-1L9.4,11.6z M10.4,16.8l-0.3-1.2C10,14.8,9.8,13.9,9.7,13
c0,0.5-0.1,0.9-0.2,1.4c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1L10.4,16.8L10.4,16.8z"/>
<path class="st0" d="M11.6,11.6h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V11.6z"/>
<path class="st0" d="M14.9,11.6h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V11.6z"/>
<path class="st0" d="M18.8,11.6h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V11.6z"/>
<path class="st0" d="M22.3,11.6h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6
l1.4,3.7h-1l-1.4-3.7v3.7h-1L22.3,11.6L22.3,11.6z M23.3,14.9c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6
c-0.1-0.2-0.1-0.3-0.2-0.4c-0.1-0.1-0.2-0.2-0.3-0.2s-0.3-0.1-0.4-0.1h-0.2v2.5H23.3z"/>
</g>
</g>
<text transform="matrix(1 0 0 1 5.4457 25.9479)" class="st0 st2 st3">BETA</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
<style type="text/css">
.st0{enable-background:new ;}
.st1{fill:#FFFFFF;}
.st2{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
.st3{fill:none;}
</style>
<g class="st0">
<path class="st1" d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z"/>
<path class="st1" d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8v0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
c0-0.6-0.3-1-0.8-1l0,0V6.8z"/>
<path class="st1" d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2s-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
s0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
c-0.2,0-0.4,0.2-0.5,0.6s-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6s0.4-0.2,0.5-0.6
C14.5,8.4,14.6,7.9,14.6,7.2z"/>
<path class="st1" d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9
s-0.1-1-0.1-1.3V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9S19,6.8,18.9,6.6c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2
h-1.8V11.4z"/>
<path class="st1" d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z"/>
</g>
<rect class="st2" width="28" height="28"/>
<g class="st0">
<path class="st1" d="M2.9,17h0.9l0.6,3c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1v-0.1L5,21.3l0.1-0.8
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2v-1v-0.9l0,0c0,0,0,0,0-0.1v0.2c0,0.2,0,0.3-0.1,0.5
s0,0.2-0.1,0.3l-0.1,0.7v0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2l-0.3,5.2h-1L2.9,17z"/>
<path class="st1" d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6
c0,0.5-0.1,0.9-0.2,1.4c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1L10.3,22.2L10.3,22.2z"/>
<path class="st1" d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z"/>
<path class="st1" d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z"/>
<path class="st1" d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z"/>
<path class="st1" d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6
l1.4,3.7h-1l-1.4-3.7v3.7h-1L22.3,17L22.3,17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6
s-0.1-0.3-0.2-0.4s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2v2.5H23.3z"/>
</g>
<rect x="-33.5" y="14" class="st3" width="8.6" height="14"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#01aeb7"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#01aeb7" stroke-miterlimit="10" stroke-width="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="#01aeb7"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 28 28" style="color:#ad0670" xml:space="preserve">
<style type="text/css">
.st1 {
fill: none;
stroke: #ad0670;
stroke-width: 2;
stroke-miterlimit: 10;
}
</style>
<g class="st0">
<path fill="currentcolor" d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z" />
<path fill="currentcolor" d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8l0,0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
c0-0.6-0.3-1-0.8-1H8V6.8z" />
<path fill="currentcolor" d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2c-0.7,0-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
c0-1.5,0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
c-0.2,0-0.4,0.2-0.5,0.6c-0.1,0.4-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6c0.2,0,0.4-0.2,0.5-0.6
C14.5,8.4,14.6,7.9,14.6,7.2z" />
<path fill="currentcolor" d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9s-0.1-1-0.1-1.3
V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9s-0.1-0.6-0.2-0.8c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2H17.2z" />
<path fill="currentcolor" d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z" />
</g>
<rect class="st1" width="28" height="28" />
<g class="st0">
<path fill="currentcolor" d="M2.9,17h0.9L4.4,20c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1c0,0,0-0.1,0-0.1L5,21.3l0.1-0.8
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2l0-1l0-0.9c0,0,0,0,0,0c0,0,0,0,0-0.1l0,0.2
c0,0.2,0,0.3-0.1,0.5s0,0.2-0.1,0.3l-0.1,0.7l0,0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2
l-0.3,5.2h-1L2.9,17z" />
<path fill="currentcolor" d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6c0,0.5-0.1,0.9-0.2,1.4
c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1H10.3z" />
<path fill="currentcolor" d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z" />
<path fill="currentcolor" d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z" />
<path fill="currentcolor" d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z" />
<path fill="currentcolor" d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6l1.4,3.7h-1
l-1.4-3.7v3.7h-1V17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6s-0.1-0.3-0.2-0.4
s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2V20.3z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
assets/frontmatter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -50,6 +50,15 @@
height: 100%;
}
.frontmatter.media_selection {
display: flex;
justify-content: center;
align-items: center;
opacity: 0.8;
text-align: center;
padding: 1rem 1.25rem;
}
.spinner,
.spinner:before,
.spinner:after {
@@ -268,6 +277,14 @@
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.base__action label {
display: block;
}
.base__action input {
width: 100% !important;
}
.seo__status__details ul > * + *,
.ext_settings > * + * {
--tw-space-y-reverse: 0;
@@ -349,4 +366,232 @@
.table__cell__validation .warning {
color: #E6AF2E;
}
/* Fields */
.field__toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.field__toggle input {
opacity: 0;
width: 0;
height: 0;
}
.field__toggle__slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--vscode-button-secondaryBackground);
-webkit-transition: .4s;
transition: .4s;
border-radius: 34px;
}
.field__toggle__slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .field__toggle__slider {
background-color: var(--vscode-button-background);
}
input:focus + .field__toggle__slider {
box-shadow: 0 0 1px var(--vscode-button-background);
}
input:checked + .field__toggle__slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Metadata */
.metadata_field {
margin-bottom: 1rem;
}
.metadata_field__label {
display: flex;
align-items: center;
margin-bottom: .5rem;
}
.metadata_field__label svg {
margin-right: .5rem;
}
.metadata_field__textarea, .metadata_field__textarea:focus {
outline: none;
}
.metadata_field__limit {
color: var(--vscode-inputValidation-warningBorder);
margin-top: .25rem;
}
.metadata_field__number {
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
outline: none !important;
}
.metadata_field__choice {
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
outline: none !important;
width: 100%;
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
color: var(--vscode-input-foreground);
background-color: var(--vscode-input-background);
}
.metadata_field__choice::placeholde {
color: var(--vscode-input-placeholderForeground);
}
.metadata_field__datetime {
display: flex;
justify-content: space-between;
}
.metadata_field__datetime > .react-datepicker-wrapper {
margin-right: 1rem;
}
.metadata_field__datetime > button {
color: var(--vscode-button-secondaryForeground);
background-color: var(--vscode-button-secondaryBackground);
padding-left: 1rem;
padding-right: 1rem;
width: auto;
}
.metadata_field__preview_image img {
display: block;
margin: 0 auto;
max-height: 16rem;
}
.metadata_field__preview_image__remove {
background-color: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-inputValidation-errorForeground);
}
.metadata_field__preview_image__remove:hover {
background-color: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-inputValidation-errorForeground);
opacity: .9;
}
/* File list */
.file_list vscode-label {
border-bottom: 1px solid var(--vscode-foreground);
}
.file_list__items {
padding: 0;
list-style: none;
}
.file_list__items__item {
color: var(--vscode-foreground);
font-size: var(--vscode-font-size);
font-weight: var(--vscode-font-weight);
cursor: pointer;
height: 22px;
line-height: 22px;
margin: 0 -1rem;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: flex-start;
}
.file_list__items__item:hover {
background-color: var(--vscode-list-hoverBackground);
color: var(--vscode-list-hoverForeground);
cursor: pointer;
}
.file_list__items__item svg {
display: block;
flex-shrink: 0;
height: 20px;
width: 20px;
margin-right: .25rem;
}
.file_list__items__item span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Sponsor */
.sponsor {
opacity: 0.5;
}
.sponsor:hover {
opacity: 1;
}
.sponsor:hover svg {
fill: currentcolor;
}
.sponsor svg {
height: 20px;
width: 20px;
margin-right: .25rem;
}
.sponsor a {
display: flex;
align-items: center;
justify-content: center;
color: var(--vscode-foreground);
text-decoration: none;
}
.sponsor a:hover {
color: var(--vscode-foreground);
text-decoration: none;
}
.sponsor a > span {
margin-right: .25rem;
}
/* Timepicker */
.react-datepicker button:hover {
background-color: none !important;
}
.react-datepicker__triangle {
transform: translate3d(15px, 0px, 0px) !important;
}
.react-datepicker-time__input {
background: transparent !important;
color: #000 !important;
}
.react-datepicker-time__input input {
border: 1px solid #aeaeae !important;
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
assets/v2.5.0/actions.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/v2.5.0/baseview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

BIN
assets/v3.0.0/baseview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
assets/v3.0.0/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
assets/v3.0.0/metadata.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

BIN
assets/v3.0.0/welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
assets/v3.1.0/media.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

BIN
assets/v4.0.0/banner-v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

BIN
assets/v4.0.0/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

1461
package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

8
postcss.config.js Normal file
View File

@@ -0,0 +1,8 @@
const tailwindcss = require('tailwindcss');
module.exports = {
plugins: [
tailwindcss('./tailwind.config.js'),
require('autoprefixer'),
],
};

20
scripts/beta-release.js Normal file
View File

@@ -0,0 +1,20 @@
const fs = require('fs');
const path = require('path');
const packageJson = require('../package.json');
const version = packageJson.version.split('.');
packageJson.version = `${version[0]}.${version[1]}.${process.argv[process.argv.length-1].substr(0, 7)}`;
packageJson.preview = true;
packageJson.name = "vscode-front-matter-beta";
packageJson.displayName = `${packageJson.displayName} BETA`;
packageJson.description = `BETA Version of Front Matter. ${packageJson.description}`;
packageJson.icon = "assets/frontmatter-beta.png";
packageJson.homepage = "https://beta.frontmatter.codes";
console.log(packageJson.version);
fs.writeFileSync(path.join(path.resolve('.'), 'package.json'), JSON.stringify(packageJson, null, 2));
let readme = fs.readFileSync(path.join(__dirname, '../README.beta.md'), 'utf8');
fs.writeFileSync(path.join(__dirname, '../README.md'), readme);

7
scripts/main-release.js Normal file
View File

@@ -0,0 +1,7 @@
const fs = require('fs');
const path = require('path');
const packageJson = require('../package.json');
packageJson.name = "vscode-front-matter";
fs.writeFileSync(path.join(path.resolve('.'), 'package.json'), JSON.stringify(packageJson, null, 2));

View File

@@ -1,15 +1,19 @@
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX } from './../constants/settings';
import * as vscode from 'vscode';
import { TaxonomyType } from "../models";
import { CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_DATE_FIELD } from "../constants/settings";
import { CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from "../constants/settings";
import { format } from "date-fns";
import { ArticleHelper, SettingsHelper, SlugHelper } from '../helpers';
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
import matter = require('gray-matter');
import { Notifications } from '../helpers/Notifications';
import { extname, basename } from 'path';
import { COMMAND_NAME, DefaultFields } from '../constants';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
export class Article {
private static prevContent = "";
/**
@@ -18,13 +22,13 @@ export class Article {
* @param type
*/
public static async insert(type: TaxonomyType) {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
const article = Article.getCurrent();
if (!article) {
return;
}
@@ -44,7 +48,7 @@ export class Article {
}
// Add all the known options to the selection list
const crntOptions = SettingsHelper.getTaxonomy(type);
const crntOptions = Settings.getTaxonomy(type);
if (crntOptions && crntOptions.length > 0) {
for (const crntOpt of crntOptions) {
if (!options.find(o => o.label === crntOpt)) {
@@ -92,7 +96,6 @@ export class Article {
ArticleHelper.update(editor, article);
} catch (e) {
Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
console.log(e.message);
}
}
@@ -101,14 +104,7 @@ export class Article {
* @param article
*/
public static updateDate(article: matter.GrayMatterFile<string>, forceCreate: boolean = false) {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const dateFormat = config.get(SETTING_DATE_FORMAT) as string;
const dateField = config.get(SETTING_DATE_FIELD) as string || "date";
const modField = config.get(SETTING_MODIFIED_FIELD) as string || "date";
article = this.articleDate(article, dateFormat, dateField, forceCreate);
article = this.articleDate(article, dateFormat, modField, false);
article.data = ArticleHelper.updateDates(article.data);
return article;
}
@@ -116,7 +112,6 @@ export class Article {
* Sets the article lastmod date
*/
public static async setLastModifiedDate() {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
@@ -128,19 +123,13 @@ export class Article {
}
const cloneArticle = Object.assign({}, article);
const dateFormat = config.get(SETTING_DATE_FORMAT) as string;
const dateField = config.get(SETTING_MODIFIED_FIELD) as string || "lastmod";
const dateField = Settings.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified;
try {
if (dateFormat && typeof dateFormat === "string") {
cloneArticle.data[dateField] = format(new Date(), dateFormat);
} else {
cloneArticle.data[dateField] = new Date().toISOString();
}
cloneArticle.data[dateField] = Article.formatDate(new Date());
ArticleHelper.update(editor, cloneArticle);
} catch (e) {
} catch (e: any) {
Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
console.log(e.message);
}
}
@@ -148,11 +137,10 @@ export class Article {
* Generate the slug based on the article title
*/
public static async generateSlug() {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const prefix = config.get(SETTING_SLUG_PREFIX) as string;
const suffix = config.get(SETTING_SLUG_SUFFIX) as string;
const updateFileName = config.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
const filePrefix = config.get<string>(SETTING_TEMPLATES_PREFIX);
const prefix = Settings.get(SETTING_SLUG_PREFIX) as string;
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
const filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const editor = vscode.window.activeTextEditor;
if (!editor) {
@@ -196,8 +184,7 @@ export class Article {
overwrite: false
});
} catch (e) {
Notifications.error(`Failed to rename file.`);
console.log(e?.message || e);
Notifications.error(`Failed to rename file: ${e?.message || e}`);
}
}
}
@@ -231,8 +218,7 @@ export class Article {
const editor = vscode.window.activeTextEditor;
if (txtChanges.length > 0 && editor && ArticleHelper.isMarkdownFile()) {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const autoUpdate = config.get(SETTING_AUTO_UPDATE_DATE);
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (autoUpdate) {
const article = ArticleHelper.getFrontMatter(editor);
@@ -251,6 +237,60 @@ export class Article {
}
}
/**
* Format the date to the defined format
*/
public static formatDate(dateValue: Date) {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
if (dateFormat && typeof dateFormat === "string") {
return format(dateValue, dateFormat);
} else {
return dateValue.toISOString();
}
}
/**
* Insert an image from the media dashboard into the article
*/
public static async insertImage() {
let editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const position = editor.selection.active;
await vscode.commands.executeCommand(COMMAND_NAME.dashboard, {
type: "media",
data: {
filePath: editor.document.uri.fsPath,
fieldName: basename(editor.document.uri.fsPath),
position
}
} as DashboardData);
// Let the editor panel know you are selecting an image
ExplorerView.getInstance().getMediaSelection();
}
/**
* Get the current article
*/
private static getCurrent(): matter.GrayMatterFile<string> | undefined {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
return article;
}
/**
* Update the article date and return it
* @param article
@@ -258,13 +298,9 @@ export class Article {
* @param field
* @param forceCreate
*/
private static articleDate(article: matter.GrayMatterFile<string>, dateFormat: string, field: string, forceCreate: boolean) {
private static articleDate(article: matter.GrayMatterFile<string>, field: string, forceCreate: boolean) {
if (typeof article.data[field] !== "undefined" || forceCreate) {
if (dateFormat && typeof dateFormat === "string") {
article.data[field] = format(new Date(), dateFormat);
} else {
article.data[field] = new Date().toISOString();
}
article.data[field] = Article.formatDate(new Date());
}
return article;
}

522
src/commands/Dashboard.ts Normal file
View File

@@ -0,0 +1,522 @@
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET } from './../constants/settings';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { basename, dirname, extname, join } from "path";
import { existsSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
import { Settings as SettingsHelper } from '../helpers';
import { TaxonomyType } from '../models';
import { Folders } from './Folders';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
import { Page } from '../dashboardWebView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { COMMAND_NAME, EXTENSION_STATE_PAGES_VIEW } from '../constants/Extension';
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Settings } from '../dashboardWebView/models/Settings';
import { Extension } from '../helpers/Extension';
import { parseJSON } from 'date-fns';
import { ViewType } from '../dashboardWebView/state';
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
import { decodeBase64Image } from '../helpers/decodeBase64Image';
import { DefaultFields } from '../constants';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
export class Dashboard {
private static webview: WebviewPanel | null = null;
private static isDisposed: boolean = true;
private static media: MediaInfo[] = [];
private static timers: { [folder: string]: any } = {};
private static _viewData: DashboardData | undefined;
public static get viewData(): DashboardData | undefined {
return Dashboard._viewData;
}
/** 
* Init the dashboard
*/
public static async init() {
const openOnStartup = SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART);
if (openOnStartup) {
Dashboard.open();
}
}
/**
* Open or reveal the dashboard
*/
public static async open(data?: DashboardData) {
Dashboard._viewData = data;
if (Dashboard.isOpen) {
Dashboard.reveal();
} else {
Dashboard.create();
}
}
/**
* Check if the dashboard is still open
*/
public static get isOpen(): boolean {
return !Dashboard.isDisposed;
}
/**
* Reveal the dashboard if it is open
*/
public static reveal() {
if (Dashboard.webview) {
Dashboard.webview.reveal();
}
}
/**
* Create the dashboard webview
*/
public static async create() {
const extensionUri = Extension.getInstance().extensionPath;
// Create the preview webview
Dashboard.webview = window.createWebviewPanel(
'frontMatterDashboard',
'FrontMatter Dashboard',
ViewColumn.One,
{
enableScripts: true
}
);
Dashboard.isDisposed = false;
Dashboard.webview.iconPath = {
dark: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter.svg'))
};
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri);
Dashboard.webview.onDidChangeViewState(() => {
if (!this.webview?.visible) {
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
}
});
Dashboard.webview.onDidDispose(() => {
Dashboard.isDisposed = true;
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
});
SettingsHelper.onConfigChange((global?: any) => {
Dashboard.getSettings();
});
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
switch(msg.command) {
case DashboardMessage.getViewType:
if (Dashboard._viewData) {
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard._viewData });
}
break;
case DashboardMessage.getData:
Dashboard.getSettings();
Dashboard.getPages();
break;
case DashboardMessage.openFile:
openFileInEditor(msg.data);
break;
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.updateSetting:
Dashboard.updateSetting(msg.data);
break;
case DashboardMessage.initializeProject:
await commands.executeCommand(COMMAND_NAME.init, Dashboard.getSettings);
break;
case DashboardMessage.reload:
if (!Dashboard.isDisposed) {
Dashboard.webview?.dispose();
setTimeout(() => {
Dashboard.open();
}, 100);
}
break;
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(EXTENSION_STATE_PAGES_VIEW, msg.data);
break;
case DashboardMessage.getMedia:
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder)
break;
case DashboardMessage.copyToClipboard:
env.clipboard.writeText(msg.data);
break;
case DashboardMessage.refreshMedia:
Dashboard.media = [];
Dashboard.getMedia(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
Dashboard.saveFile(msg?.data);
break;
case DashboardMessage.deleteMedia:
Dashboard.deleteFile(msg?.data);
break;
case DashboardMessage.insertPreviewImage:
Dashboard.insertImage(msg?.data);
break;
}
});
}
/**
* Insert an image into the front matter or contents
* @param data
*/
private static async insertImage(data: any) {
if (data?.file && data?.image) {
if (!data?.position) {
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
}
await EditorHelper.showFile(data.file);
Dashboard._viewData = undefined;
const extensionUri = Extension.getInstance().extensionPath;
const panel = ExplorerView.getInstance(extensionUri);
if (data?.position) {
const editor = window.activeTextEditor;
const line = data.position.line;
const character = data.position.character;
if (line) {
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![](${data.image})`));
}
panel.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image});
}
}
}
/**
* Retrieve the settings for the dashboard
*/
private static async getSettings() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
Dashboard.postWebviewMessage({
command: DashboardCommand.settings,
data: {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS),
folders: Folders.get(),
initialized: await Template.isInitialized(),
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<ViewType | undefined>(EXTENSION_STATE_PAGES_VIEW),
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
} as Settings
});
}
/**
* Update a setting from the dashboard
*/
private static async updateSetting(data: { name: string, value: any }) {
await SettingsHelper.update(data.name, data.value);
Dashboard.getSettings();
}
/**
* Retrieve all media files
*/
private static async getMedia(page: number = 0, folder: string = '') {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
if (Dashboard.media.length === 0) {
const contentFolder = Folders.get();
let allMedia: MediaInfo[] = [];
if (staticFolder) {
const files = await workspace.findFiles(`${staticFolder || ""}/**/*`);
const media = Dashboard.filterMedia(files);
allMedia = [...media];
}
if (contentFolder && wsFolder) {
for (let i = 0; i < contentFolder.length; i++) {
const folder = contentFolder[i];
const relFolderPath = folder.path.substring(wsFolder.fsPath.length + 1);
const files = await workspace.findFiles(`${relFolderPath}/**/*`);
const media = Dashboard.filterMedia(files);
allMedia = [...allMedia, ...media];
}
}
allMedia = allMedia.sort((a, b) => {
if (b.fsPath < a.fsPath) {
return -1;
}
if (b.fsPath > a.fsPath) {
return 1;
}
return 0;
});
Dashboard.media = Object.assign([], allMedia);
}
// Filter the media
let files: MediaInfo[] = Dashboard.media;
if (folder) {
files = files.filter(f => f.fsPath.includes(folder));
}
// Retrieve the total after filtering and before the slicing happens
const total = files.length;
// Get media set
files = files.slice(page * 16, ((page + 1) * 16));
files = files.map((file) => {
try {
return {
...file,
stats: statSync(file.fsPath)
};
} catch (e) {
return {...file, stats: undefined};
}
}).filter(f => f.stats !== undefined);
const folders = [...new Set(Dashboard.media.map((file) => {
let relFolderPath = wsFolder ? file.fsPath.substring(wsFolder.fsPath.length + 1) : file.fsPath;
if (staticFolder && relFolderPath.startsWith(staticFolder)) {
relFolderPath = relFolderPath.substring(staticFolder.length);
}
if (relFolderPath?.startsWith('/')) {
relFolderPath = relFolderPath.substring(1);
}
return dirname(relFolderPath);
}))];
Dashboard.postWebviewMessage({
command: DashboardCommand.media,
data: {
media: files,
total: total,
folders
} as MediaPaths
});
}
/**
* Retrieve all the markdown pages
*/
private static async getPages() {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.mdx`)) {
try {
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
if (article?.data.title) {
const page: Page = {
...article.data,
// FrontMatter properties
fmFolder: folder.title,
fmModified: file.mtime,
fmFilePath: file.filePath,
fmFileName: file.fileName,
fmDraft: article?.data.draft ? "Draft" : "Published",
fmYear: article?.data[dateField] ? parseJSON(article?.data[dateField]).getFullYear() : null,
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
date: article?.data[dateField] || "",
draft: article?.data.draft,
description: article?.data[descriptionField] || "",
};
if (article?.data.preview && wsFolder) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", article?.data.preview);
const contentFolderPath = join(dirname(file.filePath), article?.data.preview);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
page.preview = preview?.toString() || "";
} else {
page.preview = "";
}
}
pages.push(page);
}
} catch (error: any) {
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
}
}
Dashboard.postWebviewMessage({
command: DashboardCommand.pages,
data: pages
});
}
/**
* Filter the media files
*/
private static filterMedia(files: Uri[]) {
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext);
}).map((file) => ({
fsPath: file.fsPath,
vsPath: Dashboard.webview?.webview.asWebviewUri(file).toString(),
stats: undefined
} as MediaInfo));
}
/**
* Save the dropped file in the current folder
* @param fileData
*/
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
if (fileName && contents) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "", folder || "");
if (!existsSync(absFolderPath)) {
absFolderPath = join(wsPath, folder || "");
}
if (!existsSync(absFolderPath)) {
Notifications.error(`We couldn't find your selected folder.`);
return;
}
const staticPath = join(absFolderPath, fileName);
const imgData = decodeBase64Image(contents);
if (imgData) {
writeFileSync(staticPath, imgData.data);
Notifications.info(`File ${fileName} uploaded to: ${staticFolder}/${folder}`);
const folderPath = `${staticFolder}/${folder}`;
if (Dashboard.timers[folderPath]) {
clearTimeout(Dashboard.timers[folderPath]);
delete Dashboard.timers[folderPath];
}
Dashboard.timers[folderPath] = setTimeout(() => {
Dashboard.media = [];
Dashboard.getMedia(0, folder || "");
delete Dashboard.timers[folderPath];
}, 500);
} else {
Notifications.error(`Something went wrong uploading ${fileName}`);
}
}
}
/**
* Delete the selected file
* @param fileData
*/
private static async deleteFile({ file, page, folder }: { file: string; page: number; folder: string | null; }) {
if (!file) {
return;
}
try {
unlinkSync(file);
Dashboard.media = [];
Dashboard.getMedia(page || 0, folder || "");
} catch(err) {
Notifications.error(`Something went wrong deleting ${basename(file)}`);
}
}
/**
* Post data to the dashboard
* @param msg
*/
private static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
if (Dashboard.isDisposed) {
return;
}
if (Dashboard.webview) {
Dashboard.webview?.webview.postMessage(msg);
}
}
/**
* Retrieve the webview HTML contents
* @param webView
*/
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
const scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', 'pages.js'));
const nonce = WebviewHelper.getNonce();
const version = Extension.getInstance().getVersion();
return `
<!DOCTYPE html>
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Matter Dashboard</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
<div id="app" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`dashboard-${version.installedVersion}`}" alt="Daily usage" />
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`;
}
}

View File

@@ -1,11 +1,14 @@
import { SETTINGS_CONTENT_PAGE_FOLDERS } from './../constants/settings';
import { commands, Uri, workspace, window } from "vscode";
import { CONFIG_KEY, SETTINGS_CONTENT_FOLDERS } from "../constants";
import { basename } from "path";
import { ContentFolder, FolderInfo } from "../models";
import { basename, join } from "path";
import { ContentFolder, FileInfo, FolderInfo } from "../models";
import uniqBy = require("lodash.uniqby");
import { Template } from "./Template";
import { Notifications } from "../helpers/Notifications";
import { CONTEXT } from "../constants/context";
import { Settings } from "../helpers";
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
export class Folders {
@@ -21,9 +24,14 @@ export class Folders {
return;
}
const selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
placeHolder: `Select where you want to create your content`
});
let selectedFolder: string | undefined;
if (folders.length > 1) {
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
placeHolder: `Select where you want to create your content`
});
} else {
selectedFolder = folders[0].title;
}
if (!selectedFolder) {
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
@@ -32,7 +40,7 @@ export class Folders {
const location = folders.find(f => f.title === selectedFolder);
if (location) {
const folderPath = Folders.getFolderPath(Uri.file(location.fsPath));
const folderPath = Folders.getFolderPath(Uri.file(location.path));
if (folderPath) {
Template.create(folderPath);
}
@@ -49,7 +57,7 @@ export class Folders {
let folders = Folders.get();
const exists = folders.find(f => f.paths.includes(folder.fsPath) || f.paths.includes(wslPath));
const exists = folders.find(f => f.path.includes(folder.fsPath) || f.path.includes(wslPath));
if (exists) {
Notifications.warning(`Folder is already registered`);
@@ -64,17 +72,14 @@ export class Folders {
folders.push({
title: folderName,
fsPath: folder.fsPath,
paths: folder.fsPath === wslPath ? [folder.fsPath] : [folder.fsPath, wslPath]
path: folder.fsPath
} as ContentFolder);
folders = uniqBy(folders, f => f.fsPath);
folders = uniqBy(folders, f => f.path);
await Folders.update(folders);
Notifications.info(`Folder registered`);
}
Folders.updateVsCodeCtx();
}
/**
@@ -84,23 +89,9 @@ export class Folders {
public static async unregister(folder: Uri) {
if (folder && folder.path) {
let folders = Folders.get();
folders = folders.filter(f => f.fsPath !== folder.fsPath);
folders = folders.filter(f => f.path !== folder.fsPath);
await Folders.update(folders);
}
Folders.updateVsCodeCtx();
}
/**
* Update the registered folders context
*/
public static updateVsCodeCtx() {
const folders = Folders.get();
let allFolders: string[] = [];
for (const folder of folders) {
allFolders = [...allFolders, ...folder.paths]
}
commands.executeCommand('setContext', CONTEXT.registeredFolders, allFolders);
}
/**
@@ -110,30 +101,85 @@ export class Folders {
*/
public static getFolderPath(folder: Uri) {
let folderPath = "";
const wsFolder = Folders.getWorkspaceFolder();
if (folder && folder.fsPath) {
folderPath = folder.fsPath;
} else if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
folderPath = workspace.workspaceFolders[0].uri.fsPath;
} else if (wsFolder) {
folderPath = wsFolder.fsPath;
}
return folderPath;
}
/**
* Retrieve the workspace folder
*/
public static getWorkspaceFolder(): Uri | undefined {
const folders = workspace.workspaceFolders;
if (folders && folders.length > 0) {
return folders[0].uri;
}
return undefined;
}
/**
* Get the name of the project
*/
public static getProjectFolderName(): string {
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
// const projectFolder = wsFolder?.fsPath.split('\\').join('/').split('/').pop();
return basename(wsFolder.fsPath);
}
return "";
}
/**
* Get the registered folders information
*/
public static async getInfo(): Promise<FolderInfo[] | null> {
public static async getInfo(limit?: number): Promise<FolderInfo[] | null> {
const folders = Folders.get();
if (folders && folders.length > 0) {
let folderInfo: FolderInfo[] = [];
for (const folder of folders) {
try {
const files = await workspace.fs.readDirectory(Uri.file(folder.fsPath));
if (files) {
folderInfo.push({
title: folder.title,
files: files.length
});
const projectName = Folders.getProjectFolderName();
let projectStart = folder.path.split(projectName).pop();
if (projectStart) {
projectStart = projectStart.replace(/\\/g, '/');
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
const mdFiles = await workspace.findFiles(join(projectStart, '**/*.md'));
const mdxFiles = await workspace.findFiles(join(projectStart, '**/*.mdx'));
let files = [...mdFiles, ...mdxFiles];
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
...stats
});
} catch (error) {
// Skip the file
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
folderInfo.push({
title: folder.title,
files: files.length,
lastModified: fileStats
});
}
}
} catch (e) {
// Skip the current folder
@@ -150,10 +196,14 @@ export class Folders {
* Get the folder settings
* @returns
*/
private static get() {
const config = workspace.getConfiguration(CONFIG_KEY);
const folders: ContentFolder[] = config.get(SETTINGS_CONTENT_FOLDERS) as ContentFolder[];
return folders;
public static get(): ContentFolder[] {
const wsFolder = Folders.getWorkspaceFolder();
const folders: ContentFolder[] = Settings.get(SETTINGS_CONTENT_PAGE_FOLDERS) as ContentFolder[];
return folders.map(folder => ({
title: folder.title,
path: Folders.absWsFolder(folder, wsFolder)
}));
}
/**
@@ -161,7 +211,33 @@ export class Folders {
* @param folders
*/
private static async update(folders: ContentFolder[]) {
const config = workspace.getConfiguration(CONFIG_KEY);
await config.update(SETTINGS_CONTENT_FOLDERS, folders);
const wsFolder = Folders.getWorkspaceFolder();
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folders.map(folder => ({ title: folder.title, path: Folders.relWsFolder(folder, wsFolder) })));
}
/**
* Generate the absolute URL for the workspace
* @param folder
* @param wsFolder
* @returns
*/
private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, wsFolder?.fsPath || "");
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return absPath;
}
/**
* Generate relative folder path
* @param folder
* @param wsFolder
* @returns
*/
private static relWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(wsFolder?.fsPath || "", WORKSPACE_PLACEHOLDER);
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
return absPath;
}
}

135
src/commands/Preview.ts Normal file
View File

@@ -0,0 +1,135 @@
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME } from './../constants/settings';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
import { commands, env, Uri, ViewColumn, window } from "vscode";
import { Settings } from '../helpers';
import { PreviewSettings } from '../models';
import { format } from 'date-fns';
import { CONTEXT } from '../constants/context';
export class Preview {
/** 
* Init the preview
*/
public static async init() {
const settings = Preview.getSettings();
await commands.executeCommand('setContext', CONTEXT.canOpenPreview, !!settings.host);
}
/**
* Open the markdown preview in the editor
*/
public static async open(extensionPath: string) {
const settings = Preview.getSettings();
if (!settings.host) {
return;
}
const editor = window.activeTextEditor;
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
let slug = article?.data ? article.data.slug : "";
if (settings.pathname) {
const articleDate = ArticleHelper.getDate(article);
try {
slug = join(format(articleDate || new Date(), settings.pathname), slug);
} catch (error) {
slug = join(settings.pathname, slug);
}
}
// Create the preview webview
const webView = window.createWebviewPanel(
'frontMatterPreview',
'FrontMatter Preview',
{
viewColumn: ViewColumn.Beside,
preserveFocus: true
},
{
enableScripts: true
}
);
webView.iconPath = {
dark: Uri.file(join(extensionPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionPath, 'assets/frontmatter.svg'))
}
const localhostUrl = await env.asExternalUri(
Uri.parse(settings.host)
);
const cspSource = webView.webview.cspSource;
webView.webview.html = `<!DOCTYPE html>
<head>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; frame-src ${localhostUrl} ${cspSource} http: https:; img-src ${localhostUrl} ${cspSource} http: https:; script-src ${localhostUrl} ${cspSource} 'unsafe-inline'; style-src ${localhostUrl} ${cspSource} 'self' 'unsafe-inline' http: https:;"
/>
<style>
html,body {
margin: 0;
padding: 0;
background: white;
height: 100%;
width: 100%;
}
body {
margin: 0;
padding: 0;
}
iframe {
width: 100%;
height: calc(100% - 30px);
border: 0;
margin-top: 30px;
}
.slug {
width: 100%;
position: fixed;
height: 30px;
display: flex;
align-items: center;
background-color: var(--vscode-editor-background);
border-bottom: 1px solid var(--vscode-focusBorder);
}
input {
color: var(--vscode-editor-foreground);
padding: 0.25rem 0.5rem;
background: none;
border: 0;
width: 100%;
}
</style>
</head>
<body>
<div class="slug">
<input type="text" value="${join(localhostUrl.toString(), slug)}" disabled />
</div>
<iframe src="${join(localhostUrl.toString(), slug)}" >
</body>
</html>`;
}
/**
* Retrieve all settings related to the preview command
*/
public static getSettings(): PreviewSettings {
const host = Settings.get<string>(SETTING_PREVIEW_HOST);
const pathname = Settings.get<string>(SETTING_PREVIEW_PATHNAME);
return {
host,
pathname
};
}
}

View File

@@ -4,6 +4,8 @@ import { join } from "path";
import * as fs from "fs";
import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Settings } from "../helpers";
export class Project {
@@ -25,6 +27,8 @@ categories: []
*/
public static async init(sampleTemplate: boolean = true) {
try {
Settings.createTeamSettings();
const folder = Template.getSettings();
const templatePath = Project.templatePath();
@@ -42,7 +46,7 @@ categories: []
fs.writeFileSync(article.fsPath, Project.content, { encoding: "utf-8" });
Notifications.info("Project initialized successfully.");
}
} catch (err) {
} catch (err: any) {
Notifications.error(`Sorry, something went wrong - ${err?.message || err}`);
}
}
@@ -52,14 +56,13 @@ categories: []
*/
public static templatePath() {
const folder = Template.getSettings();
const workspaceFolders = workspace.workspaceFolders;
const wsFolder = Folders.getWorkspaceFolder();
if (!folder || !workspaceFolders || workspaceFolders.length === 0) {
if (!folder || !wsFolder) {
return null;
}
const workspaceFolder = workspaceFolders[0];
const templatePath = Uri.file(join(workspaceFolder.uri.fsPath, folder));
const templatePath = Uri.file(join(wsFolder.fsPath, folder));
return templatePath;
}
}

View File

@@ -2,8 +2,8 @@ import * as vscode from 'vscode';
import * as matter from 'gray-matter';
import * as fs from 'fs';
import { TaxonomyType } from "../models";
import { CONFIG_KEY, SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, EXTENSION_NAME } from '../constants';
import { ArticleHelper, SettingsHelper, FilesHelper } from '../helpers';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, EXTENSION_NAME } from '../constants';
import { ArticleHelper, Settings as SettingsHelper, FilesHelper } from '../helpers';
import { TomlEngine, getFmLanguage, getFormatOpts } from '../helpers/TomlEngine';
import { DumpOptions } from 'js-yaml';
import { Notifications } from '../helpers/Notifications';
@@ -22,9 +22,8 @@ export class Settings {
});
if (newOption) {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const configSetting = type === TaxonomyType.Tag ? SETTING_TAXONOMY_TAGS : SETTING_TAXONOMY_CATEGORIES;
let options = config.get(configSetting) as string[];
let options = SettingsHelper.get(configSetting, true) as string[];
if (!options) {
options = [];
}
@@ -35,7 +34,7 @@ export class Settings {
}
options.push(newOption);
await SettingsHelper.update(type, options);
await SettingsHelper.updateTaxonomy(type, options);
// Ask if the new term needs to be added to the page
const addToPage = await vscode.window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: `Do you want to add the new ${type === TaxonomyType.Tag ? "tag" : "category"} to the page?` });
@@ -72,8 +71,6 @@ export class Settings {
* Export the tags/categories front matter to the user settings
*/
public static async export() {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
// Retrieve all the Markdown files
const allMdFiles = await FilesHelper.getMdFiles();
if (!allMdFiles) {
@@ -128,22 +125,22 @@ export class Settings {
}
// Retrieve the currently known tags, and add the new ones
let crntTags: string[] = config.get(SETTING_TAXONOMY_TAGS) as string[];
let crntTags: string[] = SettingsHelper.get(SETTING_TAXONOMY_TAGS, true) as string[];
if (!crntTags) { crntTags = []; }
crntTags = [...crntTags, ...tags];
// Update the tags and filter out the duplicates
crntTags = [...new Set(crntTags)];
crntTags = crntTags.sort().filter(t => !!t);
await config.update(SETTING_TAXONOMY_TAGS, crntTags);
await SettingsHelper.update(SETTING_TAXONOMY_TAGS, crntTags, true);
// Retrieve the currently known tags, and add the new ones
let crntCategories: string[] = config.get(SETTING_TAXONOMY_CATEGORIES) as string[];
let crntCategories: string[] = SettingsHelper.get(SETTING_TAXONOMY_CATEGORIES, true) as string[];
if (!crntCategories) { crntCategories = []; }
crntCategories = [...crntCategories, ...categories];
// Update the categories and filter out the duplicates
crntCategories = [...new Set(crntCategories)];
crntCategories = crntCategories.sort().filter(c => !!c);
await config.update(SETTING_TAXONOMY_CATEGORIES, crntCategories);
await SettingsHelper.update(SETTING_TAXONOMY_CATEGORIES, crntCategories, true);
// Done
Notifications.info(`Export completed. Tags: ${crntTags.length} - Categories: ${crntCategories.length}.`);
@@ -155,8 +152,6 @@ export class Settings {
* Remap a tag or category to a new one
*/
public static async remap() {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const taxType = await vscode.window.showQuickPick([
"Tag",
"Category"
@@ -253,7 +248,6 @@ export class Settings {
}
}
} catch (e) {
console.log(file.path);
// Continue with the next file
}
}
@@ -272,7 +266,7 @@ export class Settings {
// Remove the selected option
options = options.filter(o => o !== selectedOption);
}
await SettingsHelper.update(type, options);
await SettingsHelper.updateTaxonomy(type, options);
Notifications.info(`${newOptionValue ? "Remapping" : "Deleation"} of the ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"} completed.`);
});

View File

@@ -1,8 +1,8 @@
import { SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants/settings';
import * as vscode from 'vscode';
import { CONFIG_KEY } from '../constants';
import { ArticleHelper, SeoHelper } from '../helpers';
import { ExplorerView } from '../webview/ExplorerView';
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
import { ExplorerView } from '../explorerView/ExplorerView';
import { DefaultFields } from '../constants';
export class StatusListener {
@@ -23,7 +23,6 @@ export class StatusListener {
// Update the StatusBar based on the article draft state
if (article && typeof article.data["draft"] !== "undefined") {
// console.log(`Draft status: ${article.data["draft"]}`);
if (article.data["draft"] === true) {
frontMatterSB.text = `$(book) ${draftMsg}`;
frontMatterSB.show();
@@ -38,10 +37,9 @@ export class StatusListener {
collection.clear();
// Retrieve the SEO config properties
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const titleLength = config.get(SETTING_SEO_TITLE_LENGTH) as number || -1;
const descLength = config.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1;
const fieldName = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || "description";
const titleLength = Settings.get(SETTING_SEO_TITLE_LENGTH) as number || -1;
const descLength = Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1;
const fieldName = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
if (article.data.title && titleLength > -1) {
SeoHelper.checkLength(editor, collection, article, "title", titleLength);

View File

@@ -1,14 +1,15 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { CONFIG_KEY, SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { format } from 'date-fns';
import sanitize from '../helpers/Sanitize';
import { ArticleHelper } from '../helpers';
import { ArticleHelper, Settings } from '../helpers';
import { Article } from '.';
import { Notifications } from '../helpers/Notifications';
import { CONTEXT } from '../constants/context';
import { Project } from './Project';
import { Folders } from './Folders';
export class Template {
@@ -24,15 +25,14 @@ export class Template {
* Check if the project is already initialized
*/
public static async isInitialized() {
const workspaceFolders = vscode.workspace.workspaceFolders;
const wsFolder = Folders.getWorkspaceFolder();
const folder = Template.getSettings();
if (!folder || !workspaceFolders || workspaceFolders.length === 0) {
if (!folder || !wsFolder) {
return false;
}
const workspaceFolder = workspaceFolders[0];
const templatePath = vscode.Uri.file(path.join(workspaceFolder.uri.fsPath, folder));
const templatePath = vscode.Uri.file(path.join(wsFolder.fsPath, folder));
try {
await vscode.workspace.fs.stat(templatePath);
@@ -93,9 +93,8 @@ export class Template {
* Create from a template
*/
public static async create(folderPath: string) {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const folder = config.get<string>(SETTING_TEMPLATES_FOLDER);
const prefix = config.get<string>(SETTING_TEMPLATES_PREFIX);
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
if (!folderPath) {
Notifications.warning(`Incorrect project folder path retrieved.`);
@@ -188,8 +187,7 @@ export class Template {
* Get the folder settings
*/
public static getSettings() {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
const folder = config.get<string>(SETTING_TEMPLATES_FOLDER);
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
return folder;
}
}

View File

@@ -0,0 +1,44 @@
import { ContentType } from './../models/PanelSettings';
export const DEFAULT_CONTENT_TYPE_NAME = 'default';
export const DEFAULT_CONTENT_TYPE: ContentType = {
"name": "default",
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime"
},
{
"title": "Article preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
};

View File

@@ -0,0 +1,6 @@
export const DefaultFields = {
PublishingDate: `date`,
LastModified: `lastmod`,
Description: `description`
};

View File

@@ -1,5 +1,11 @@
const extensionName = "frontMatter";
export const EXTENSION_ID = 'eliostruyf.vscode-front-matter';
export const EXTENSION_BETA_ID = 'eliostruyf.vscode-front-matter-beta';
export const EXTENSION_STATE_VERSION = 'frontMatter:Version';
export const EXTENSION_STATE_PAGES_VIEW = 'frontMatter:Pages:ViewType';
export const getCommandName = (command: string) => {
return `${extensionName}.${command}`;
};
@@ -12,7 +18,6 @@ export const COMMAND_NAME = {
createCategory: getCommandName("createCategory"),
exportTaxonomy: getCommandName("exportTaxonomy"),
remap: getCommandName("remap"),
setDate: getCommandName("setDate"),
setLastModifiedDate: getCommandName("setLastModifiedDate"),
generateSlug: getCommandName("generateSlug"),
createFromTemplate: getCommandName("createFromTemplate"),
@@ -22,4 +27,8 @@ export const COMMAND_NAME = {
createContent: getCommandName("createContent"),
createTemplate: getCommandName("createTemplate"),
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
promote: getCommandName("promoteSettings"),
insertImage: getCommandName("insertImage"),
};

4
src/constants/Links.ts Normal file
View File

@@ -0,0 +1,4 @@
export const GITHUB_LINK = "https://github.com/estruyf/vscode-front-matter";
export const ISSUE_LINK = "https://github.com/estruyf/vscode-front-matter/issues";
export const SPONSOR_LINK = "https://github.com/sponsors/estruyf";
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";

View File

@@ -1,6 +1,5 @@
export const CONTEXT = {
canInit: "frontMatterCanInit",
registeredFolders: 'frontMatter.registeredFolders'
canOpenPreview: "frontMatterCanOpenPreview",
canOpenDashboard: "frontMatterCanOpenDashboard"
};

View File

@@ -1 +1,7 @@
export * from './DefaultFields';
export * from './Extension';
export * from './Links';
export * from './charMap';
export * from './context';
export * from './settings';
export * from './stopwords-en';

View File

@@ -5,6 +5,8 @@ export const CONFIG_KEY = "frontMatter";
export const SETTING_TAXONOMY_TAGS = "taxonomy.tags";
export const SETTING_TAXONOMY_CATEGORIES = "taxonomy.categories";
export const SETTING_DATE_FORMAT = "taxonomy.dateFormat";
export const SETTING_COMMA_SEPARATED_FIELDS = "taxonomy.commaSeparatedFields";
export const SETTING_TAXONOMY_CONTENT_TYPES = "taxonomy.contentTypes";
export const SETTING_DATE_FIELD = "taxonomy.dateField";
export const SETTING_MODIFIED_FIELD = "taxonomy.modifiedField";
@@ -27,8 +29,20 @@ export const SETTING_TEMPLATES_PREFIX = "templates.prefix";
export const SETTING_PANEL_FREEFORM = "panel.freeform";
export const SETTING_PREVIEW_HOST = "preview.host";
export const SETTING_PREVIEW_PATHNAME = "preview.pathName";
export const SETTING_CUSTOM_SCRIPTS = "custom.scripts";
export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
export const SETTINGS_CONTENT_STATIC_FOLDERS = "content.publicFolder";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
/**
* @deprecated
*/
export const SETTINGS_CONTENT_FOLDERS = "content.folders";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";

View File

@@ -0,0 +1,7 @@
export enum DashboardCommand {
loading = "loading",
pages = "pages",
settings = "settings",
media = "media",
viewData = "viewData",
}

View File

@@ -0,0 +1,17 @@
export enum DashboardMessage {
getViewType = 'getViewType',
getData = 'getData',
openFile = 'openFile',
getTheme = 'getTheme',
createContent = 'createContent',
updateSetting = 'updateSetting',
initializeProject = 'initializeProject',
reload = 'reload',
setPageViewType = 'setPageViewType',
getMedia = 'getMedia',
copyToClipboard = 'copyToClipboard',
refreshMedia = 'refreshMedia',
uploadMedia = 'uploadMedia',
deleteMedia = 'deleteMedia',
insertPreviewImage = 'insertPreviewImage',
}

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
export interface IButtonProps {
disabled?: boolean;
onClick: () => void;
}
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, disabled, children}: React.PropsWithChildren<IButtonProps>) => {
return (
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { Header } from '../Header';
import { Overview } from './Overview';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
export interface IContentsProps {
pages: Page[];
loading: boolean;
}
export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loading}: React.PropsWithChildren<IContentsProps>) => {
const settings = useRecoilValue(SettingsSelector);
const pageFolders = [...new Set(pages.map(page => page.fmFolder))];
return (
<main className={`h-full w-full`}>
<div className="flex flex-col h-full overflow-auto">
<Header
folders={pageFolders}
totalPages={pages.length}
settings={settings} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
{ loading ? <Spinner /> : <Overview pages={pages} settings={settings} /> }
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
</div>
</main>
);
};

View File

@@ -0,0 +1,70 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models/Page';
import { ViewSelector, ViewType } from '../../state';
import { DateField } from '../DateField';
import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
export interface IItemProps extends Page {}
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, preview }: React.PropsWithChildren<IItemProps>) => {
const view = useRecoilValue(ViewSelector);
const openFile = () => {
Messenger.send(DashboardMessage.openFile, fmFilePath);
};
if (view === ViewType.Grid) {
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
{
preview ? (
<img src={`${preview}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
) : (
<div className={`flex items-center justify-center bg-whisper-500 dark:bg-vulcan-200 dark:group-hover:bg-vulcan-100`}>
<MarkdownIcon className={`h-32 text-vulcan-100 dark:text-whisper-100`} />
</div>
)
}
</div>
<div className="p-4 w-full">
<div className={`flex justify-between items-center`}>
<Status draft={!!draft} />
<DateField value={date} />
</div>
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<p className="text-xs text-vulcan-200 dark:text-whisper-800">{description}</p>
</div>
</button>
</li>
);
} else if (view === ViewType.List) {
return (
<li className="relative">
<button className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`} onClick={openFile}>
<div className="col-span-8 font-bold truncate">
{title}
</div>
<div className="col-span-2">
<DateField value={date} />
</div>
<div className="col-span-2">
<Status draft={!!draft} />
</div>
</button>
</li>
);
}
return null;
};

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { ViewSelector, ViewType } from '../../state';
export interface IListProps {}
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
const view = useRecoilValue(ViewSelector);
let className = '';
if (view === ViewType.Grid) {
className = `grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`;
} else if (view === ViewType.List) {
className = `-mx-4`;
}
return (
<ul role="list" className={className}>
{view === ViewType.List && (
<li className="px-5 relative uppercase text-vulcan-100 dark:text-whisper-900 py-2 border-b border-vulcan-50">
<div className={`grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8`}>
<div className="col-span-8">
Title
</div>
<div className="col-span-2">
Date
</div>
<div className="col-span-2">
Status
</div>
</div>
</li>
)}
{children}
</ul>
);
};

View File

@@ -0,0 +1,88 @@
import { Disclosure } from '@headlessui/react';
import { ChevronRightIcon } from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { groupBy } from '../../../helpers/GroupBy';
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
import { GroupOption } from '../../constants/GroupOption';
import { Page } from '../../models/Page';
import { Settings } from '../../models/Settings';
import { GroupingSelector } from '../../state';
import { Item } from './Item';
import { List } from './List';
export interface IOverviewProps {
pages: Page[];
settings: Settings | null;
}
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages, settings}: React.PropsWithChildren<IOverviewProps>) => {
const grouping = useRecoilValue(GroupingSelector);
if (!pages || !pages.length) {
return (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
{
settings && settings?.folders?.length > 0 ? (
<p className={`text-xl font-medium`}>No Markdown to show</p>
) : (
<>
<p className={`text-lg font-medium`}>Make sure you registered a content folder in your project to let Front Matter find the contents.</p>
</>
)
}
</div>
</div>
);
}
if (grouping !== GroupOption.none) {
const groupedPages = groupBy(pages, grouping === GroupOption.Year ? 'fmYear' : 'fmDraft');
let groupKeys = Object.keys(groupedPages);
if (grouping === GroupOption.Year) {
groupKeys = groupKeys.sort((a, b) => { return parseInt(b) - parseInt(a) });
}
return (
<>
{
groupKeys.map((groupId, idx) => (
<Disclosure key={groupId} as={`div`} className={`w-full`} defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className={`mb-4 ${idx !== 0 ? "mt-8" : ""}`}>
<h2 className={`text-2xl font-bold flex items-center`}>
<ChevronRightIcon
className={`w-8 h-8 mr-1 ${open ? "transform rotate-90" : ""}`}
/>
{GroupOption[grouping]}: {groupId} ({groupedPages[groupId].length})
</h2>
</Disclosure.Button>
<Disclosure.Panel>
<List>
{groupedPages[groupId].map((page: Page) => (
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>
</Disclosure.Panel>
</>
)}
</Disclosure>
))
}
</>
);
}
return (
<List>
{pages.map((page, idx) => (
<Item key={`${page.slug}-${idx}`} {...page} />
))}
</List>
);
};

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import { Spinner } from './Spinner';
import useMessages from '../hooks/useMessages';
import useDarkMode from '../../hooks/useDarkMode';
import usePages from '../hooks/usePages';
import { WelcomeScreen } from './WelcomeScreen';
import { useRecoilValue } from 'recoil';
import { DashboardViewSelector, ViewDataAtom } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
export interface IDashboardProps {
showWelcome: boolean;
}
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome}: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages, settings } = useMessages();
const { pageItems } = usePages(pages);
const view = useRecoilValue(DashboardViewSelector);
useDarkMode();
if (!settings) {
return <Spinner />;
}
if (showWelcome) {
return <WelcomeScreen settings={settings} />;
}
if (!settings.initialized || settings.folders?.length === 0) {
return <WelcomeScreen settings={settings} />;
}
if (view === 'media') {
return <Media />;
}
return <Contents pages={pageItems} loading={loading} />;
};

View File

@@ -0,0 +1,28 @@
import { format, parseJSON } from 'date-fns';
import * as React from 'react';
export interface IDateFieldProps {
value: Date | string;
}
export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: React.PropsWithChildren<IDateFieldProps>) => {
const [ dateValue, setDateValue ] = React.useState<string>("");
React.useEffect(() => {
try {
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
setDateValue(dateString);
} catch (e) {
// Date is invalid
}
}, [value]);
if (!dateValue) {
return null;
}
return (
<span className={`text-vulcan-100 dark:text-whisper-900 text-xs`}>{dateValue}</span>
);
};

View File

@@ -0,0 +1,54 @@
import { XCircleIcon } from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { SortingSelector, FolderSelector, TagSelector, CategorySelector, SortingAtom, DEFAULT_SORTING_OPTION, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
import { DefaultValue } from 'recoil';
export const guardRecoilDefaultValue = (
candidate: any
): candidate is DefaultValue => {
if (candidate instanceof DefaultValue) return true;
return false;
};
export interface IClearFiltersProps {}
export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props: React.PropsWithChildren<IClearFiltersProps>) => {
const [ show, setShow ] = React.useState(false);
const sorting = useRecoilValue(SortingSelector);
const folder = useRecoilValue(FolderSelector);
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
const reset = () => {
setShow(false);
resetSorting();
resetFolder();
resetTag();
resetCategory();
};
React.useEffect(() => {
if (sorting !== DEFAULT_SORTING_OPTION || folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
setShow(true);
} else {
setShow(false);
}
}, [sorting, folder, tag, category]);
if (!show) return null;
return (
<button className="flex items-center hover:text-teal-600" onClick={reset} title={`Clear filters, grouping, and sorting`}>
<XCircleIcon className={`inline-block w-5 h-5 mr-1`} /><span>Clear</span>
<span className={`sr-only`}> filters, grouping, and sorting</span>
</button>
);
};

View File

@@ -0,0 +1,51 @@
import { Menu } from '@headlessui/react';
import { FilterIcon } from '@heroicons/react/solid';
import * as React from 'react';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface IFilterProps {
label: string;
items: string[];
activeItem: string | null;
onClick: (item: string | null) => void;
}
const DEFAULT_VALUE = "No filter";
export const Filter: React.FunctionComponent<IFilterProps> = ({label, activeItem, items, onClick}: React.PropsWithChildren<IFilterProps>) => {
if (!items || items.length === 0) {
return null;
}
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton
label={(
<>
<FilterIcon className={`inline-block w-5 h-5 mr-1`} /><span>{label}</span>
</>
)}
title={activeItem || DEFAULT_VALUE} />
<MenuItems>
<MenuItem
title={DEFAULT_VALUE}
value={null}
isCurrent={!!activeItem}
onClick={() => onClick(null)} />
{items.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === activeItem}
onClick={() => onClick(option)} />
))}
</MenuItems>
</Menu>
</div>
);
};

View File

@@ -0,0 +1,44 @@
import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { FolderAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface IFoldersProps {
folders: string[];
}
const DEFAULT_TYPE = "All types";
export const Folders: React.FunctionComponent<IFoldersProps> = ({folders}: React.PropsWithChildren<IFoldersProps>) => {
const [ crntFolder, setCrntFolder ] = useRecoilState(FolderAtom);
if (folders.length <= 1) {
return null;
}
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Showing`} title={crntFolder || DEFAULT_TYPE} />
<MenuItems>
<MenuItem
title={DEFAULT_TYPE}
value={null}
isCurrent={!crntFolder}
onClick={(value) => setCrntFolder(value)} />
{folders.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === crntFolder}
onClick={(value) => setCrntFolder(value)} />
))}
</MenuItems>
</Menu>
</div>
);
};

View File

@@ -0,0 +1,39 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { GroupOption } from '../../constants/GroupOption';
import { GroupingAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface IGroupingProps {}
export const groupOptions = [
{ name: "None", id: GroupOption.none },
{ name: "Year", id: GroupOption.Year },
{ name: "Draft/Published", id: GroupOption.Draft },
];
export const Grouping: React.FunctionComponent<IGroupingProps> = ({}: React.PropsWithChildren<IGroupingProps>) => {
const [ group, setGroup ] = useRecoilState(GroupingAtom);
const crntGroup = groupOptions.find(x => x.id === group);
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Group by`} title={crntGroup?.name || ""} />
<MenuItems>
{groupOptions.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === crntGroup?.id}
onClick={(value) => setGroup(value)} />
))}
</MenuItems>
</Menu>
</div>
);
};

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import { Sorting } from './Sorting';
import { Searchbox } from './Searchbox';
import { Filter } from './Filter';
import { Folders } from './Folders';
import { Settings } from '../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Startup } from '../Startup';
import { Button } from '../Button';
import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, TagAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { PhotographIcon } from '@heroicons/react/outline';
import { Pagination } from '../Media/Pagination';
export interface IHeaderProps {
settings: Settings | null;
// Navigation
totalPages?: number;
// Page folders
folders?: string[];
}
export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folders, settings }: React.PropsWithChildren<IHeaderProps>) => {
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
const [ view, setView ] = useRecoilState(DashboardViewAtom);
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
};
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
<div className={`px-4 bg-gray-50 dark:bg-vulcan-50 border-b-2 border-gray-200 dark:border-vulcan-200`}>
<div className={`flex items-center justify-start`}>
<button className={`p-2 flex items-center ${view === "contents" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => setView("contents")}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</button>
<button className={`p-2 flex items-center ${view === "media" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => setView("media")}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</button>
</div>
</div>
{
view === "contents" && (
<>
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
<Searchbox />
<div className={`flex items-center space-x-4`}>
<Startup settings={settings} />
<Button onClick={createContent} disabled={!settings?.initialized}>Create content</Button>
</div>
</div>
<div className="px-4 flex flex-row items-center border-b border-gray-200 dark:border-vulcan-100 justify-between">
<div>
<Navigation totalPages={totalPages || 0} />
</div>
<div>
<ViewSwitch />
</div>
</div>
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end space-x-4 lg:space-x-6 xl:space-x-8 bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100`}>
<ClearFilters />
<Folders folders={folders || []} />
<Filter label={`Tag`} activeItem={crntTag} items={settings?.tags || []} onClick={(value) => setCrntTag(value)} />
<Filter label={`Category`} activeItem={crntCategory} items={settings?.categories || []} onClick={(value) => setCrntCategory(value)} />
<Grouping />
<Sorting />
</div>
</>
)
}
{
view === "media" && (
<Pagination />
)
}
</div>
);
};

View File

@@ -0,0 +1,42 @@
import { FilterIcon, SearchIcon } from '@heroicons/react/solid';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { SearchAtom } from '../../state';
export interface ISearchboxProps {}
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({}: React.PropsWithChildren<ISearchboxProps>) => {
const [ value, setValue ] = React.useState('');
const [ , setDebounceValue ] = useRecoilState(SearchAtom);
const debounceSearch = useDebounce<string>(value, 500);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
React.useEffect(() => {
setDebounceValue(debounceSearch);
}, [debounceSearch]);
return (
<div className="flex space-x-4">
<div className="flex-1 min-w-0">
<label htmlFor="search" className="sr-only">Search</label>
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="search"
name="search"
className={`block w-full py-2 pl-10 pr-3 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`}
placeholder="Search"
value={value}
onChange={handleChange}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,40 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { SortOption } from '../../constants/SortOption';
import { SearchSelector, SortingAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface ISortingProps {}
export const sortOptions = [
{ name: "Last modified", id: SortOption.LastModified },
{ name: "By filename (asc)", id: SortOption.FileNameAsc },
{ name: "By filename (desc)", id: SortOption.FileNameDesc },
];
export const Sorting: React.FunctionComponent<ISortingProps> = ({}: React.PropsWithChildren<ISortingProps>) => {
const [ crntSorting, setCrntSorting ] = useRecoilState(SortingAtom);
const searchValue = useRecoilValue(SearchSelector);
const crntSort = sortOptions.find(x => x.id === crntSorting);
return (
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={`Sort by`} title={crntSort?.name || ""} disabled={!!searchValue} />
<MenuItems>
{sortOptions.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === crntSorting}
onClick={(value) => setCrntSorting(value)} />
))}
</MenuItems>
</Menu>
</div>
);
};

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ViewAtom, ViewType, SettingsSelector } from '../../state';
import { ViewGridIcon, ViewListIcon } from '@heroicons/react/solid';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
export interface IViewSwitchProps {}
export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: React.PropsWithChildren<IViewSwitchProps>) => {
const [ view, setView ] = useRecoilState(ViewAtom);
const settings = useRecoilValue(SettingsSelector);
const toggleView = () => {
const newView = view === ViewType.Grid ? ViewType.List : ViewType.Grid;
setView(newView);
Messenger.send(DashboardMessage.setPageViewType, newView);
};
React.useEffect(() => {
if (settings?.pageViewType) {
setView(settings?.pageViewType);
}
}, [settings?.pageViewType]);
return (
<div className={`flex rounded-sm bg-vulcan-50 lg:mb-1`}>
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === ViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<ViewGridIcon className={`w-4 h-4`} />
<span className={`sr-only`}>Change to grid</span>
</button>
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === ViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<ViewListIcon className={`w-4 h-4`} />
<span className={`sr-only`}>Change to list</span>
</button>
</div>
);
};

View File

@@ -0,0 +1,6 @@
export * from './Filter';
export * from './Folders';
export * from './Grouping';
export * from './Header';
export * from './Searchbox';
export * from './Sorting';

View File

@@ -0,0 +1,77 @@
import { Menu } from '@headlessui/react';
import { XIcon } from '@heroicons/react/outline';
import Downshift from 'downshift';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaFoldersSelector, SelectedMediaFolderAtom } from '../../state';
export interface IFolderSelectionProps {}
export const FolderSelection: React.FunctionComponent<IFolderSelectionProps> = (props: React.PropsWithChildren<IFolderSelectionProps>) => {
const folders = useRecoilValue(MediaFoldersSelector);
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ focus, setFocus ] = React.useState(false);
let allFolders: string[] = Object.assign([], folders);
allFolders = allFolders.sort((a: string, b: string) => {
if (a.toLowerCase() < b.toLowerCase()) return -1;
if (a.toLowerCase() > b.toLowerCase()) return 1;
return 0;
});
return (
<div>
<Downshift
isOpen={focus}
selectedItem={selectedFolder}
onOuterClick={() => setFocus(false)}
onSelect={(selFolder) => {
setSelectedFolder(selFolder);
setFocus(false);
}}>
{
({
getInputProps,
getItemProps,
getMenuProps,
isOpen,
inputValue,
getRootProps
}) => (
<div className={`relative flex items-center`}>
<label className={`text-sm text-gray-500 dark:text-whisper-900`}>Filter by: </label>
<div
className={`inline-flex items-center`}
{...getRootProps({} as any, {suppressRefError: true})}
>
<input disabled={!!selectedFolder} onFocus={() => setFocus(true)} className={`ml-2 py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`} {...getInputProps()} />
{
selectedFolder && (
<button title={`Clear`} onClick={() => setSelectedFolder(null)}><XIcon className={`ml-2 h-6 w-6 text-red-500 hover:text-red-800`} /></button>
)
}
</div>
<div className={`${focus ? `block` : `hidden`} top-8 absolute right-0 z-10 mt-2 w-min rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`} {...getMenuProps()}>
{isOpen
? allFolders
.filter((item: string) => !inputValue || item.includes(inputValue))
.map((item, index) => (
<div
className="cursor-pointer text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100"
{...getItemProps({ key: item, index, item })}
>
{item}
</div>
))
: null}
</div>
</div>
)
}
</Downshift>
</div>
);
};

View File

@@ -0,0 +1,185 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { Alert } from '../Modals/Alert';
export interface IItemProps {
media: MediaInfo;
}
export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWithChildren<IItemProps>) => {
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ , setLightbox ] = useRecoilState(LightboxAtom);
const [ showAlert, setShowAlert ] = React.useState(false);
const viewData = useRecoilValue(ViewDataSelector);
const parseWinPath = (path: string | undefined) => {
return path?.split(`\\`).join(`/`);
}
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
let relPath = media.fsPath.split(settings.wsFolder).pop();
if (settings.staticFolder && relPath) {
relPath = relPath.split(settings.staticFolder).pop();
}
return dirname(parseWinPath(relPath) || "");
}
return "";
};
const getRelPath = () => {
let relPath: string | undefined = "";
if (settings?.wsFolder && media.fsPath) {
relPath = media.fsPath.split(settings.wsFolder).pop();
if (settings.staticFolder && relPath) {
relPath = relPath.split(settings.staticFolder).pop();
}
}
return relPath;
};
const copyToClipboard = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || "");
};
const insertToArticle = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
position: viewData?.data?.position || null
});
};
const insertSnippet = () => {
const relPath = getRelPath();
Messenger.send(DashboardMessage.insertPreviewImage, {
image: parseWinPath(relPath) || "",
file: viewData?.data?.filePath,
fieldName: viewData?.data?.fieldName,
position: viewData?.data?.position || null,
snippet: settings?.mediaSnippet.join("\n").replace("{mediaUrl}", parseWinPath(relPath) || "")
});
};
const deleteMedia = () => {
setShowAlert(true);
};
const confirmDeletion = () => {
Messenger.send(DashboardMessage.deleteMedia, {
file: media.fsPath,
folder: selectedFolder
});
};
const calculateSize = () => {
if (media?.stats?.size) {
const size = media.stats.size / (1024*1024);
if (size > 1) {
return `${size.toFixed(2)} MB`;
} else {
return `${(size * 1024).toFixed(2)} KB`;
}
}
};
const openLightbox = () => {
setLightbox(media.vsPath || "");
};
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
</div>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />
</div>
</button>
<div className={`relative py-4 pl-4 pr-10`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-2`}>
{
viewData?.data?.filePath ? (
<>
<button
title={`Insert into your article`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={insertToArticle}>
<CheckCircleIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Insert into your article</span>
</button>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<button
title={`Insert your media snippet`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={insertSnippet}>
<CodeIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Insert your media snippet</span>
</button>
)
}
</>
) : (
<>
<button title={`Copy media path`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={copyToClipboard}>
<ClipboardCopyIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Copy media path</span>
</button>
<button title={`Delete media`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Delete media</span>
</button>
</>
)
}
</div>
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center">
{basename(parseWinPath(media.fsPath) || "")}
</p>
<p className="mt-2 text-sm dark:text-whisper-900 font-medium pointer-events-none flex items-center">
<b className={`mr-2`}>Folder:</b> {getFolder()}
</p>
{
media?.stats?.size && (
<p className="mt-2 text-sm dark:text-whisper-900 font-medium pointer-events-none flex items-center">
<b className={`mr-1`}>Size:</b> {calculateSize()}
</p>
)
}
</div>
</li>
{
showAlert && (
<Alert
title={`Delete: ${basename(parseWinPath(media.fsPath) || "")}`}
description={`Are you sure you want to delete the file from the ${getFolder()} folder?`}
okBtnText={`Delete`}
cancelBtnText={`Cancel`}
dismiss={() => setShowAlert(false)}
trigger={confirmDeletion} />
)
}
</>
);
};

View File

@@ -0,0 +1,26 @@
import { basename } from 'path';
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { LightboxAtom } from '../../state';
export interface ILightboxProps {}
export const Lightbox: React.FunctionComponent<ILightboxProps> = (props: React.PropsWithChildren<ILightboxProps>) => {
const [ lightbox, setLightbox ] = useRecoilState(LightboxAtom);
if (!lightbox) {
return null;
}
const hideLightbox = () => {
setLightbox(null);
};
return (
<div onClick={hideLightbox} className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-50 z-50`}>
<div className={`w-full h-full flex flex-wrap items-center justify-center`}>
<img src={lightbox} alt={basename(lightbox)} className={`w-1/2 h-auto rounded-lg border border-vulcan-500 shadow-2xl`} />
</div>
</div>
);
};

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
export interface IListProps {}
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
return (
<ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
{children}
</ul>
);
};

View File

@@ -0,0 +1,117 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { UploadIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
import { DashboardCommand } from '../../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { Header } from '../Header';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
import { Item } from './Item';
import { Lightbox } from './Lightbox';
import { List } from './List';
import { useDropzone } from 'react-dropzone'
import { useCallback } from 'react';
import { DashboardMessage } from '../../DashboardMessage';
export interface IMediaProps {}
export const LIMIT = 16;
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
const viewData = useRecoilValue(ViewDataSelector);
const onDrop = useCallback((acceptedFiles: File[]) => {
acceptedFiles.forEach((file) => {
const reader = new FileReader();
reader.onload = () => {
const contents = reader.result;
Messenger.send(DashboardMessage.uploadMedia, {
fileName: file.name,
contents,
folder: selectedFolder
});
};
reader.readAsDataURL(file)
});
}, [selectedFolder]);
const {getRootProps, isDragActive} = useDropzone({
onDrop,
accept: 'image/*'
});
const messageListener = (message: MessageEvent<EventData<MediaPaths>>) => {
if (message.data.command === DashboardCommand.media) {
setLoading(false);
setMedia(message.data.data.media);
setTotal(message.data.data.total);
setFolders(message.data.data.folders);
}
};
React.useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, ['']);
return (
<main className={`h-full w-full`}>
<div className="flex flex-col h-full overflow-auto">
<Header settings={settings} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
{
viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>Select the image you want to use for your article.</p>
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
</div>
)
}
{
isDragActive && (
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
<UploadIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
</p>
</div>
)
}
<List>
{
media.map((file) => (
<Item key={file.fsPath} media={file} />
))
}
</List>
</div>
{
loading && ( <Spinner /> )
}
<Lightbox />
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
</div>
</main>
);
};

View File

@@ -0,0 +1,130 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { RefreshIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { DashboardMessage } from '../../DashboardMessage';
import { LoadingAtom, MediaTotalSelector, SelectedMediaFolderSelector } from '../../state';
import { FolderSelection } from './FolderSelection';
import { LIMIT } from './Media';
import { PaginationButton } from './PaginationButton';
export interface IPaginationProps {}
export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.PropsWithChildren<IPaginationProps>) => {
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const totalMedia = useRecoilValue(MediaTotalSelector);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const [ page, setPage ] = React.useState<number>(0);
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
if (totalMedia < mediaItems) {
return totalMedia;
}
return mediaItems;
};
// Write me function to retrieve buttons before and after current page
const getButtons = (): number[] => {
const maxButtons = 5;
const buttons: number[] = [];
const start = page - maxButtons;
const end = page + maxButtons;
for (let i = start; i <= end; i++) {
if (i >= 0 && i <= totalPages) {
buttons.push(i);
}
}
return buttons;
};
const refresh = () => {
setPage(0);
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || ''
});
}, [page]);
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page: 0,
folder: selectedFolder || ''
});
setPage(0);
}, [selectedFolder]);
return (
<nav
className="py-4 px-5 flex items-center justify-between bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100"
aria-label="Pagination"
>
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
</p>
</div>
<FolderSelection />
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
<PaginationButton
title="First"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(0)
}
}} />
<PaginationButton
title="Previous"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(page - 1)
}
}} />
{getButtons().map((button) => (
<button
key={button}
disabled={button === page}
onClick={() => {
setPage(button)
}
}
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'}`}
>{button + 1}</button>
))}
<PaginationButton
title="Next"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalPages}
onClick={() => setPage(totalPages)} />
</div>
</nav>
);
};

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
export interface IPaginationButtonProps {
title: string;
disabled?: boolean;
onClick: () => void;
}
export const PaginationButton: React.FunctionComponent<IPaginationButtonProps> = ({title, disabled, onClick}: React.PropsWithChildren<IPaginationButtonProps>) => {
return (
<button
disabled={disabled}
onClick={onClick}
className={`text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500 disabled:hover:text-gray-500 dark:disabled:hover:text-whisper-900 disabled:opacity-50`}
>
{title}
</button>
);
};

View File

@@ -0,0 +1,25 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/solid';
import * as React from 'react';
export interface IMenuButtonProps {
label: string | JSX.Element;
title: string;
disabled?: boolean;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({label, title, disabled}: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div className={`inline-flex items-center ${disabled ? 'opacity-50' : ''}`}>
<span className={`text-gray-500 dark:text-whisper-700 mr-2 font-medium`}>{label}:</span>
<Menu.Button disabled={disabled} className="group inline-flex justify-center text-sm font-medium text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600">
{title}
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
aria-hidden="true"
/>
</Menu.Button>
</div>
);
};

View File

@@ -0,0 +1,22 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
export interface IMenuItemProps {
title: string;
value: any;
isCurrent: boolean;
onClick: (value: any) => void;
}
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
return (
<Menu.Item>
<button
onClick={() => onClick(value)}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100`}
>
{title}
</button>
</Menu.Item>
);
};

View File

@@ -0,0 +1,25 @@
import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment } from 'react';
export interface IMenuItemsProps {}
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({children}: React.PropsWithChildren<IMenuItemsProps>) => {
return (
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto">
<div className="py-1">
{children}
</div>
</Menu.Items>
</Transition>
);
};

View File

@@ -0,0 +1,3 @@
export * from './MenuButton';
export * from './MenuItem';
export * from './MenuItems';

View File

@@ -0,0 +1,89 @@
import { Dialog, Transition, } from '@headlessui/react';
import { ExclamationIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { Fragment, useRef } from 'react';
export interface IAlertProps {
title: string;
description: string;
okBtnText: string;
cancelBtnText: string;
dismiss: () => void;
trigger: () => void;
}
export const Alert: React.FunctionComponent<IAlertProps> = ({title, description, cancelBtnText, okBtnText, dismiss, trigger}: React.PropsWithChildren<IAlertProps>) => {
const cancelButtonRef = useRef(null);
return (
<Transition.Root show={true} as={Fragment}>
<Dialog className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef} onClose={() => dismiss()}>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white dark:bg-vulcan-500 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 border-whisper-900">
<div className="sm:flex sm:items-start">
<div className="mt-3 mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:mx-0 sm:h-10 sm:w-10 bg-gray-50 dark:bg-vulcan-400">
<ExclamationIcon className="h-6 w-6 text-red-500 dark:text-red-50" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-vulcan-300 dark:text-whisper-900">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-vulcan-500 dark:text-whisper-500">
{description}
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 dark:hover:bg-red-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => trigger()}
>
{okBtnText}
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-200 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm"
onClick={() => dismiss()}
ref={cancelButtonRef}
>
{cancelBtnText}
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { useRecoilState } from 'recoil';
import { Tab } from '../constants/Tab';
import { TabAtom } from '../state';
export interface INavigationProps {
totalPages: number;
}
export const tabs = [
{ name: 'All articles', id: Tab.All},
{ name: 'Published', id: Tab.Published },
{ name: 'In draft', id: Tab.Draft }
];
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
return (
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
aria-current={tab.id === crntTab ? 'page' : undefined}
onClick={() => setCrntTab(tab.id)}
>
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
</button>
))}
</nav>
);
};

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
export interface ISpinnerProps {}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
return (
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
</div>
);
};

View File

@@ -0,0 +1,23 @@
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { REVIEW_LINK, SPONSOR_LINK } from '../../constants/Links';
import { VersionInfo } from '../../models';
export interface ISponsorMsgProps {
beta: boolean | undefined;
version: VersionInfo | undefined;
}
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
return (
<p className={`w-full max-w-7xl mx-auto px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center justify-between`}>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={SPONSOR_LINK} title="Sponsor Front Matter">
<span>Sponsor</span> <HeartIcon className={`h-5 w-5 group-hover:fill-current`} />
</a>
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
<a className={`group inline-flex justify-center items-center space-x-2 text-vulcan-500 dark:text-whisper-500 hover:text-vulcan-600 dark:hover:text-whisper-300 opacity-50 hover:opacity-100`} href={REVIEW_LINK} title="Review Front Matter">
<StarIcon className={`h-5 w-5 group-hover:fill-current`} /> <span>Review</span>
</a>
</p>
);
};

View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import { SETTINGS_DASHBOARD_OPENONSTART } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../DashboardMessage';
import { Settings } from '../models/Settings';
export interface IStartupProps {
settings: Settings | null;
}
export const Startup: React.FunctionComponent<IStartupProps> = ({settings}: React.PropsWithChildren<IStartupProps>) => {
const [isChecked, setIsChecked] = React.useState(false);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(e.target.checked);
Messenger.send(DashboardMessage.updateSetting, { name: SETTINGS_DASHBOARD_OPENONSTART, value: e.target.checked });
};
React.useEffect(() => {
setIsChecked(!!settings?.openOnStart);
}, [settings?.openOnStart]);
return (
<div className={`relative flex items-start`}>
<div className="flex items-center h-5">
<input
id="startup"
aria-describedby="startup-description"
name="startup"
type="checkbox"
checked={isChecked}
onChange={onChange}
className="focus:outline-none focus:ring-teal-500 h-4 w-4 text-teal-600 border-gray-300 dark:border-vulcan-50 rounded"
/>
</div>
<div className="ml-2 text-sm">
<label id="startup-description" htmlFor="startup" className="font-medium text-vulcan-50 dark:text-whisper-900">
Open on startup?
</label>
</div>
</div>
);
};

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
export interface IStatusProps {
draft: boolean;
}
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
return (
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
);
};

View File

@@ -0,0 +1,60 @@
import { CheckIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { Status } from '../../models/Status';
export interface IStepProps {
name: string;
description: string;
status: Status;
showLine: boolean;
onClick?: () => void;
}
export const Step: React.FunctionComponent<IStepProps> = ({name, description, status, showLine, onClick}: React.PropsWithChildren<IStepProps>) => {
return (
<>
{
showLine ? (
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
) : null
}
<button className={`relative flex items-start group text-left ${onClick ? "" : "cursor-default"}`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
{
status === Status.NotStarted && (
<span className="h-9 flex items-center" aria-hidden="true">
<span className={`relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 rounded-full ${onClick ? "group-hover:text-gray-400" : ""}`}>
<span className={`h-2.5 w-2.5 bg-transparent rounded-full ${onClick ? "group-hover:bg-gray-300" : ""}`} />
</span>
</span>
)
}
{
status === Status.Active && (
<span className="h-9 flex items-center" aria-hidden="true">
<span className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-teal-600 rounded-full">
<span className="h-2.5 w-2.5 bg-teal-600 rounded-full" />
</span>
</span>
)
}
{
status === Status.Completed && (
<span className="h-9 flex items-center">
<span className="relative z-10 w-8 h-8 flex items-center justify-center bg-teal-600 rounded-full group-hover:bg-teal-800">
<CheckIcon className="w-5 h-5 text-white" aria-hidden="true" />
</span>
</span>
)
}
<span className="ml-4 min-w-0 flex flex-col">
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
<span className="text-sm text-vulcan-400 dark:text-whisper-600" dangerouslySetInnerHTML={{__html: description}} />
</span>
</button>
</>
);
};

View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { Settings } from '../../models/Settings';
import { Status } from '../../models/Status';
import { Step } from './Step';
export interface IStepsToGetStartedProps {
settings: Settings;
}
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
const steps = [
{
name: 'Initialize project',
description: 'Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.',
status: settings.initialized ? Status.Completed : Status.NotStarted,
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
},
{
name: 'Register content folders (manual action)',
description: 'Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.',
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
},
{
name: 'Show the dashboard',
description: 'Once both actions are completed, click on this action to load the dashboard.',
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
}
];
return (
<nav aria-label="Progress">
<ol role="list" className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />
</li>
))}
</ol>
</nav>
);
};

View File

@@ -0,0 +1,92 @@
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants/Links';
import { Messenger } from '@estruyf/vscode/dist/client';
import { FrontMatterIcon } from '../../panelWebView/components/Icons/FrontMatterIcon';
import { GitHubIcon } from '../../panelWebView/components/Icons/GitHubIcon';
import { DashboardMessage } from '../DashboardMessage';
import { Settings } from '../models/Settings';
import { StepsToGetStarted } from './Steps/StepsToGetStarted';
export interface IWelcomeScreenProps {
settings: Settings;
}
export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({settings}: React.PropsWithChildren<IWelcomeScreenProps>) => {
React.useEffect(() => {
return () => {
Messenger.send(DashboardMessage.reload)
};
}, ['']);
return (
<div className={`h-full overflow-auto py-24`}>
<main>
<div className="mx-auto max-w-7xl">
<div className="grid grid-cols-12 gap-8">
<div className="px-6 col-span-8 text-left flex items-center">
<div>
<h1 className="mt-4 text-4xl tracking-tight font-extrabold sm:mt-5 sm:leading-none lg:mt-6 lg:text-5xl xl:text-6xl">
<span className="md:block">Manage your static site with</span>{' '}
<span className="text-teal-500 md:block">Front Matter</span>
</h1>
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
Thank you for taking the time to test out Front Matter!
</p>
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
We try to aim to make Front Matter as easy to use as possible, but if you have any questions, please don't hesitate to reach out to us on GitHub.
</p>
<div className="mt-5 w-full sm:mx-auto sm:max-w-lg lg:ml-0">
<div className="flex flex-wrap items-start justify-between">
<a href={GITHUB_LINK} title={`GitHub`} className="flex items-center px-1 text-vulcan-300 hover:text-vulcan-500 dark:text-whisper-500 dark:hover:text-teal-500">
<GitHubIcon className="w-8 h-8" />
<span className={`text-lg ml-2`}>GitHub / Documentation</span>
</a>
<a href={SPONSOR_LINK} title={`Become a sponsor`} className="flex items-center px-1 text-vulcan-300 hover:text-vulcan-500 dark:text-whisper-500 dark:hover:text-teal-500">
<HeartIcon className="w-8 h-8" />
<span className={`text-lg ml-2`}>Sponsor</span>
</a>
<a href={REVIEW_LINK} title={`Write a quick review`} className="flex items-center px-1 text-vulcan-300 hover:text-vulcan-500 dark:text-whisper-500 dark:hover:text-teal-500">
<StarIcon className="w-8 h-8" />
<span className={`text-lg ml-2`}>Review</span>
</a>
</div>
</div>
</div>
</div>
<div className="col-span-4 flex justify-center items-center">
<FrontMatterIcon className={`h-64 w-64 text-vulcan-500 dark:text-whisper-500`} />
</div>
</div>
</div>
<div className="mx-auto max-w-7xl mt-12 px-6">
<h2 className="text-xl tracking-tight font-extrabold sm:leading-none">
Perform the next steps to get you started with the extension
</h2>
<div className={`grid grid-cols-12 gap-8 mt-5`}>
<div className={`col-span-8`}>
<StepsToGetStarted settings={settings} />
<p className="mt-5 text-sm text-vulcan-300 dark:text-whisper-700">
Once you completed both actions, the dashboard will show its full potential. You can also use the extension from the <b>Front Matter</b> side panel. There you will find the actions you can perform specifically for your pages.
</p>
</div>
</div>
<h2 className="mt-5 text-lg tracking-tight font-extrabold sm:leading-none">
We hope you enjoy Front Matter!
</h2>
</div>
</main>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More