Compare commits

..

877 Commits

Author SHA1 Message Date
a95c78fd60 unused-rm
Some checks failed
deploy / test (push) Failing after 6m54s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-03 00:01:01 +03:00
4e050198c5 pathfix2
Some checks failed
deploy / test (push) Failing after 6m52s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-02 23:33:58 +03:00
36810d688c fmt2
Some checks failed
deploy / test (push) Failing after 1m13s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-02 23:24:32 +03:00
abc1f184a1 fmt
Some checks failed
deploy / test (push) Failing after 1m10s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-02 23:21:35 +03:00
244c91fb02 pathfix
Some checks failed
deploy / test (push) Failing after 43s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-02 23:17:12 +03:00
6bd919f16b views-refactored
Some checks failed
deploy / test (push) Failing after 1m9s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-02 23:12:14 +03:00
Tony
04878bec0b
Merge pull request #487 from Discours/hotfix/sv-artical-component
Fix slug updating in ArticlePage component by managing currentSlug signal
2024-10-02 23:10:32 +03:00
b791de3d4e fmt
Some checks failed
deploy / test (push) Failing after 1m7s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-02 21:29:51 +03:00
6f26e09bef drafts-view-refactoring+fix
Some checks failed
deploy / Update templates on Mailgun (push) Waiting to run
deploy / test (push) Has been cancelled
2024-10-02 21:29:32 +03:00
Stepan Vladovskiy
55e2b34466 feat: fix slug updating in ArticlePage component by managing curentSlug signal 2024-10-01 20:50:01 +00:00
217c027044 ..
Some checks failed
deploy / test (push) Failing after 1m12s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-01 22:52:45 +03:00
8106bae0c2 editor-refactored
Some checks failed
deploy / test (push) Failing after 1m15s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-01 22:39:17 +03:00
ae1a93469b bubble-menu-used
Some checks failed
deploy / test (push) Failing after 6m42s
deploy / Update templates on Mailgun (push) Has been skipped
2024-10-01 21:11:07 +03:00
67e8c80d9a fmt 2024-10-01 20:19:40 +03:00
abdc419aa8 mini+micro-fix 2024-10-01 20:18:27 +03:00
Stepan Vladovskiy
32ebcff5fe feat: improve component structure and addded signal managment. Extract Article page Content into a separate block. Added createSignal and CreateEffect to track slug dynamically. Replace ifelse with Switch and Match. 2024-09-30 16:35:15 +00:00
Stepan Vladovskiy
b91a1be989 Merge branch 'dev' into hotfix/sv-author-empty 2024-09-30 14:30:21 +00:00
21b3903062 minor-fix 2024-09-30 14:00:02 +03:00
fab8a5ed53 minor
Some checks failed
deploy / test (push) Failing after 1m6s
deploy / Update templates on Mailgun (push) Has been skipped
2024-09-30 13:38:44 +03:00
845490d5db Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into dev 2024-09-27 21:12:07 +03:00
30de1ddb3e editor-refactored-2 2024-09-27 21:09:50 +03:00
90cd3988a1 editor-toolbar 2024-09-27 21:00:45 +03:00
258b579d05 editor-toolbar 2024-09-27 20:57:49 +03:00
76dea4341d toolbar-appear-fix 2024-09-27 20:21:52 +03:00
595e2b8a4b editor-refactoring 2024-09-27 19:31:54 +03:00
b393810f7a cleanup-stories 2024-09-27 17:28:50 +03:00
90057a2d0e fmt 2024-09-27 17:27:01 +03:00
7aa01d6152 MiniEditor-fix 2024-09-27 17:26:40 +03:00
962140e755 microeditor-wip 2024-09-27 16:46:43 +03:00
0c61445293 config-fix
Some checks failed
deploy / test (push) Failing after 48s
deploy / Update templates on Mailgun (push) Has been skipped
2024-09-27 10:51:51 +03:00
29dbd67c27 debugL 2024-09-25 21:15:17 -03:00
a144d7051b expo-fixes 2024-09-26 01:48:30 +03:00
c165742fbf debug: replace createAsync for slugloader to createSignal in slug/tab for trying avoid complex repeating of async functions 2024-09-25 19:16:13 -03:00
bbab1e4cb8 debug: Added logic for robust reload data forslug before render 2024-09-25 17:25:08 -03:00
dc2b6a5ab1 debug: with reset data if slug is changes in slug/tab articalpage 2024-09-25 17:04:08 -03:00
91d8fdf746 debug: add logs in dev variant of slug/tab 2024-09-25 16:20:42 -03:00
db8b53f9bd debug: back to dev version. Investigate UserPage Artcicle Render 2024-09-25 10:21:08 -03:00
aa11c8d8b8 debug: With load topics and logs in slug tab to see why it is not loading topic from user page 2024-09-25 09:59:12 -03:00
6dc25260bb debug: break everythig, rename Artile Page and add logs 2024-09-24 22:25:56 -03:00
a805493b27 debug: used version only with useParam and with logging for slug tab 2024-09-24 22:14:46 -03:00
996153bbc2 debug: more pres logs for slug tab.tsx 2024-09-24 22:03:57 -03:00
c26f960d9b debug: slug with logging to async and useParam for fetching data and slugs 2024-09-24 21:47:19 -03:00
ef7ceb5d33 debug: backed slug to dev code just rename ArticlePage inner function 2024-09-24 20:51:32 -03:00
22575cc7fa expo-showup 2024-09-24 14:56:16 +03:00
a52ee5a90f Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into dev 2024-09-24 14:26:58 +03:00
2fef053029 canedit-fix 2024-09-24 14:24:53 +03:00
5e2b4a7ae6 canedit-fix 2024-09-24 13:09:24 +03:00
6e3871cd5a link-fix1 2024-09-24 12:38:53 +03:00
a451a6bf4e Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into dev 2024-09-24 12:19:40 +03:00
317d4a004c graphql-client-unmemo
Some checks failed
deploy / test (push) Failing after 6m50s
deploy / Update templates on Mailgun (push) Has been skipped
2024-09-24 12:15:50 +03:00
29d1661993 new-shout-desc-fix
Some checks failed
deploy / test (push) Failing after 6m49s
deploy / Update templates on Mailgun (push) Has been skipped
2024-09-24 11:03:40 +03:00
7288725480 new-draft-store-fix 2024-09-24 10:59:54 +03:00
c959a2bba4 fmt
Some checks failed
deploy / test (push) Failing after 7m1s
deploy / Update templates on Mailgun (push) Has been skipped
2024-09-24 09:51:58 +03:00
d7a5a188ff editor-showup+fixes
Some checks failed
deploy / Update templates on Mailgun (push) Waiting to run
deploy / test (push) Has been cancelled
2024-09-24 09:48:39 +03:00
b3b8e51d2d devruntime-fix 2024-09-24 06:50:44 +03:00
343b71defd feat: renamed ArticalPage to component, added useParam for slug and reactivity 2024-09-24 00:40:57 -03:00
Tony
545728ac84
Merge pull request #485 from Discours/hotfix/sv-author-empty
Author Profile when logged in infinity load resolve bugFix
2024-09-23 14:17:46 +03:00
fe76bb46e6 Merge branch 'dev' into hotfix/sv-author-empty 2024-09-23 10:17:24 +03:00
1f0e91d2b9 bump 2024-09-23 10:14:33 +03:00
Stepan Vladovskiy
76366a01d9 feat: with setFollowingsLoaded(true) in Author View component. It is need if user already loaded, then after changeFollowing for unsetting Loading and show page 2024-09-20 23:56:25 +00:00
d496760fdb sbtest 2024-09-19 20:26:58 +03:00
3da600dacc packages-update 2024-09-19 20:24:11 +03:00
f9ed111d8e stylefixes 2024-09-19 20:08:22 +03:00
392581f50d linted 2024-09-19 19:58:23 +03:00
c40a357815 Merge branch 'storybook' of https://github.com/discours/discoursio-webapp into storybook 2024-09-19 19:57:55 +03:00
6ca29a351f editor-wip 2024-09-19 19:51:56 +03:00
d6c6545726 Merge branch 'dev' of https://github.com/discours/discoursio-webapp into dev 2024-09-19 19:15:57 +03:00
1db4224827 editor-wip 2024-09-19 19:15:51 +03:00
98f03c2296 linted 2024-09-19 19:15:05 +03:00
Tony
9491f7c504
Merge pull request #483 from Discours/hotfix/sv-author-empty
Author empty for logged in user (user profile)
2024-09-19 18:12:14 +03:00
Raksana Karlova
fac7451fc4 added styles for button in SB 2024-09-17 19:52:57 +04:00
64224720f5 build-tuning 2024-09-16 16:50:44 +03:00
d1ff340e0e makePersisted-update 2024-09-16 16:35:48 +03:00
92ec0da0bc Merge branch 'dev' of https://github.com/discours/discoursio-webapp into dev 2024-09-16 15:57:24 +03:00
d3cd027910 build-fix+const-fix+icon-fix+expo-fix 2024-09-16 15:57:00 +03:00
Tony
146f41e167
Merge pull request #480 from Discours/fix/style
Style fixes
2024-09-16 14:28:07 +03:00
08cbfa1d57 vite535 2024-09-16 14:07:30 +03:00
8345d858ed vite535 2024-09-16 13:52:33 +03:00
5e73863679 local-build-ok 2024-09-16 13:45:30 +03:00
ff541b4456 freezefix 2024-09-16 13:34:17 +03:00
d210587b74 bun-ci 2024-09-16 13:21:45 +03:00
64658f5175 versions... 2024-09-16 13:14:08 +03:00
b201163ab9 buildable 2024-09-16 13:02:34 +03:00
11c94da20d lockfix 2024-09-16 12:39:55 +03:00
63e159822a build-fix 2024-09-16 12:31:12 +03:00
d5c5eaf57b packages-revert-fix 2024-09-16 12:25:28 +03:00
6a48b53216 patch-fix 2024-09-16 12:03:43 +03:00
20e7588019 force-use-dart 2024-09-16 11:56:22 +03:00
80dbe410ab try-patch-sass 2024-09-16 11:53:14 +03:00
d00084183d ci-failok 2024-09-16 11:09:27 +03:00
bf09277822 sass-embedded-downgrade 2024-09-16 10:50:47 +03:00
35febd9ac4 noapi 2024-09-16 04:06:17 +03:00
35cae9415d scss-fixes 2024-09-16 03:42:55 +03:00
74725df0ff editor-storybook-fix 2024-09-16 03:09:07 +03:00
5eded9f143 lock-restored 2024-09-16 03:03:25 +03:00
afa69fc86f editor-fix 2024-09-16 02:53:49 +03:00
6e0a830168 abc-sort-fix 2024-09-16 02:41:48 +03:00
433a74a58a linted 2024-09-15 23:17:21 +03:00
53299fc183 simplified-story-fixes 2024-09-15 23:17:02 +03:00
ebed7f38c3 sass-fixes+minieditor-storybooked 2024-09-15 22:39:39 +03:00
199f845610 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into dev 2024-09-15 21:54:40 +03:00
2fad5b8db9 bad update 2 2024-09-15 21:47:21 +03:00
344f716d1d bad update 2024-09-15 21:47:14 +03:00
ba55780246 linted-update 2024-09-15 21:44:10 +03:00
512c65aeef Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into dev 2024-09-15 19:41:05 +03:00
ad4bda3c24 prestorybook 2024-09-15 19:41:02 +03:00
Tony
7d895aa343
Merge pull request #481 from Discours/storybook
Storybook
2024-09-15 19:38:22 +03:00
Stepan Vladovskiy
49001bb63c style: with small readme at top, and small coment in code 2024-09-15 15:37:27 +00:00
Stepan Vladovskiy
bef13a9bde feat: router/author/slug with err_ in console , and Placeholder handle undefinde in slug for users with empty slug 2024-09-14 16:30:47 +00:00
09243925b4 vite-conf-connection-fix 2024-09-12 09:56:53 +03:00
Raksana Karlova
1da37e7a52 config 2024-09-11 16:50:28 +04:00
479a4ea852 Fixed paddings on the feed page 2024-09-10 22:49:09 +03:00
Raksana Karlova
e4a1679052 Storybook update 2024-09-10 17:58:19 +04:00
8824fbab2f reactions-store-fix 2024-09-06 08:33:44 +03:00
6ec271fe7c reactions-store-fix 2024-09-06 08:27:48 +03:00
260b95f692 scrollto+shoutreaction 2024-09-06 08:13:24 +03:00
1ec368eae7 minor fixes 2024-09-06 07:55:57 +03:00
e03971193e url-backward-compat 2024-09-06 07:52:39 +03:00
6e11e19f0a Style fixes 2024-09-04 22:48:18 +03:00
d003df96f2 preload-trans 2024-09-03 20:12:46 +03:00
dd02c46174 typefix 2024-09-03 20:07:46 +03:00
9319c8d526 locnochunk 2024-09-03 19:59:53 +03:00
7a2043f223 comments-view-fix 2024-09-03 19:50:26 +03:00
a2999e3851 chunk-merged 2024-09-03 19:24:50 +03:00
23e9ca9838 fmt 2024-09-03 19:22:52 +03:00
b868c7282d postmerge 2024-09-03 19:22:33 +03:00
124ae3dece Merge remote-tracking branch 'origin/dev' into dev 2024-09-03 19:20:56 +03:00
a4e5d8f332 revert-manual-chunks 2024-09-03 19:19:24 +03:00
7fa17cee3c manual-chunked 2024-09-03 18:37:57 +03:00
6bfaa3fb51 topic-feed-revised 2024-09-03 14:36:33 +03:00
30ff30d099 fmt 2024-09-03 13:29:01 +03:00
7714977391 commentsByAuthor 2024-09-03 13:21:59 +03:00
e176544e36 author-shouts-loadmore 2024-09-03 11:07:32 +03:00
33a81d8ee7 storybook 2024-09-03 10:06:39 +03:00
7c614c66d9 minor-ref 2024-08-30 16:45:17 +03:00
cd436dd34d about-title-fix 2024-08-29 18:34:13 +03:00
0698900f8d fmt 2024-08-29 17:40:31 +03:00
090295327f profile-settings-fix 2024-08-29 16:11:51 +03:00
f808bd2394 topic+author-loadmore-fix 2024-08-28 16:10:00 +03:00
22f44ee0ec tab-navigation-fix 2024-08-28 12:50:04 +03:00
7ca7acc487 loadmore+viewed-fix 2024-08-28 11:53:40 +03:00
3d2125d99f undefer-fix 2024-08-28 08:12:44 +03:00
e695c7847f loading-error-fix 2024-08-28 01:02:55 +03:00
95198e9791 topic-page-fix 2024-08-28 00:51:17 +03:00
71772ae0d6 all-authors-revert 2024-08-27 20:17:24 +03:00
28f3d6619d fix from 'origin/demo-fix3' 2024-08-26 21:22:27 +03:00
Igor Lobanov
5bf8340f13 topic articles fix 2024-08-26 14:55:01 +02:00
tony
1c2b53cfe8 topics-fmt-fix 2024-08-26 15:26:00 +03:00
tony
0748aa342c header-auth-nav-fix 2024-08-26 15:23:51 +03:00
e360cdf1ba header-links-fix 2024-08-22 16:27:09 +03:00
17750a7630 fixd 2024-08-22 15:56:12 +03:00
b80a497e4d bump-deploy 2024-08-22 15:45:36 +03:00
ae3473312a nitro-timing-log 2024-08-14 15:50:01 +03:00
a186dc4d3f vercel-edge-deploy-test 2024-08-12 14:42:31 +03:00
e6e19d74cd netlify-runtime 2024-08-12 01:21:01 +03:00
6a105b24fc api-up 2024-08-07 13:39:39 +03:00
50d98deadf api-revert
Some checks failed
deploy / test (push) Failing after 14s
deploy / Update templates on Mailgun (push) Has been skipped
2024-08-06 18:19:38 +03:00
582a80d34d apifix 2024-08-06 15:08:04 +03:00
c6ea1eb8b7 e2e-testing 2024-08-05 15:09:57 +03:00
e797e8ca96 lock+fmt 2024-08-05 15:02:13 +03:00
e07ace8741 all-topics-cache-fix 2024-08-05 14:03:30 +03:00
b53f83947c inbox-route 2024-08-02 00:32:52 +03:00
a39190e2b1 editor fixed 2024-08-01 00:41:42 +03:00
e875212ae7 editor-fixed 2024-07-31 02:20:54 +03:00
ca629e8c26 tests 2024-07-31 01:39:24 +03:00
8db9f4c93f signup-e2e 2024-07-31 00:52:25 +03:00
ae4ec7b6b7 e2e-wip 2024-07-30 22:44:53 +03:00
6db49c2296 e2e 2024-07-30 22:28:28 +03:00
a5bb138fc7 editor-wip 2024-07-30 22:18:16 +03:00
142c5bfb41 graphql-fix 2024-07-30 22:06:17 +03:00
81ed60a52f Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-07-30 20:59:30 +03:00
418572af88 author-profile-preload-fix 2024-07-30 20:59:12 +03:00
Tony
4902096e48
Merge pull request #478 from Discours/router-upgrade
update router workflow
2024-07-30 06:04:14 +03:00
5846d24c10 alltopics-title-fix 2024-07-30 05:55:12 +03:00
4cb888262d restored-all-authors 2024-07-30 05:35:27 +03:00
1e4138e40e reset1 2024-07-26 18:49:15 +03:00
219e3e2325 :Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-07-26 01:19:05 +03:00
3e25997d2a version 2024-07-26 01:17:24 +03:00
38f6cbbdca
Debug: cleaning, back to last configs 2024-07-25 09:33:32 -03:00
15233db9a5
Debug: trying to follower read me file and place config.ts in utils 2024-07-24 22:20:38 -03:00
87dd66ad4b biomepass 2024-07-23 17:00:33 +03:00
0de087d999 comments-tree-fix 2024-07-23 16:58:57 +03:00
42d8e7598c empty-files-removed 2024-07-22 17:40:26 +03:00
b5e25e0d6d fixtest 2024-07-22 17:15:47 +03:00
1f58385b95 noauthor-vercelbuildtest 2024-07-22 17:11:43 +03:00
9733eff104 postmerge 2024-07-22 17:07:04 +03:00
607d986293 Merge branch 'router-upgrade' of github.com:Discours/discoursio-webapp into router-upgrade 2024-07-22 15:31:18 +03:00
Stepan Vladovskiy
e6aa3a881f feat: page AllAuthors show sorted list and ABC nav is working. But pagination not yet 2024-07-22 11:43:56 +00:00
b61b19a119 fmt 2024-07-22 14:29:33 +03:00
01e7dec615 author-feed-debug 2024-07-22 14:24:36 +03:00
1a9529d9fc debug... 2024-07-22 09:57:48 +03:00
85e0a92b31 editor... 2024-07-22 09:29:57 +03:00
751157b421 clued 2024-07-22 08:41:23 +03:00
0061b68257 postmerge: loadmorewrappers 2024-07-21 17:26:03 +03:00
87d08dcb75 Merge branch 'router-upgrade' into feature/rating 2024-07-21 15:48:38 +03:00
1d540f28b7 bump 2024-07-21 15:43:45 +03:00
cad695dc59 struct 2024-07-21 05:17:42 +03:00
77d8ca352a upd
Some checks failed
deploy / test (push) Failing after 5m41s
deploy / Update templates on Mailgun (push) Has been skipped
2024-07-18 15:29:23 +03:00
a8d778b2e4 buildfix2
Some checks failed
deploy / test (push) Failing after 13s
deploy / Update templates on Mailgun (push) Has been skipped
2024-07-18 14:26:57 +03:00
8f9dea9bfb buildfix 2024-07-18 14:22:59 +03:00
1eb9c57f0d loadmore-main-fix 2024-07-18 13:22:58 +03:00
7573c6334c mainpage-loadmore-hide 2024-07-18 12:22:28 +03:00
ab05a9e539 typefix 2024-07-18 07:34:55 +03:00
8c5ef2fd54 mainpage-deploy 2024-07-18 07:33:23 +03:00
8fbc85615c author-feed+comments-paginate 2024-07-16 03:14:08 +03:00
82904bd1da @-patch
Some checks failed
deploy / test (push) Failing after 14s
deploy / Update templates on Mailgun (push) Has been skipped
2024-07-16 02:11:01 +03:00
19bae3b2dd edit-new-wip 2024-07-16 01:57:44 +03:00
c79b0451cb linking-fix 2024-07-16 01:13:19 +03:00
8cce8d897e loadmore+feed+my 2024-07-15 23:56:40 +03:00
4fe2768329 load-more-main-ok 2024-07-15 23:35:33 +03:00
f6f012449d load-wrapper-debug 2024-07-15 19:51:52 +03:00
789a7497a3 load-more-wrapper-wip 2024-07-15 17:28:08 +03:00
2b7a825bc5 sortfn-type 2024-07-13 20:22:32 +03:00
4efec31fec https 2024-07-13 20:15:41 +03:00
03787196a9 minor-configs 2024-07-13 19:29:17 +03:00
35c5a0ebcf getThumbUrl 2024-07-13 15:25:56 +03:00
7c9c155f5b sort 2024-07-13 14:47:31 +03:00
2d7fbc42a8 utils-refactored 2024-07-13 14:42:53 +03:00
95612eb7b8 topic-outing+fixes 2024-07-13 13:33:49 +03:00
24e594138f topic-!-routing 2024-07-13 13:32:27 +03:00
17d2600142 filerouter-import-workaround 2024-07-13 12:45:10 +03:00
fde2335a02 @-routing-fix 2024-07-13 12:36:23 +03:00
ef1408327f isolate-utils-authors-with-@ 2024-07-13 12:06:49 +03:00
645e65751b typecheck-fix 2024-07-13 10:53:35 +03:00
f916e9f9ae nostat 2024-07-13 10:44:51 +03:00
e5a3788f71 vercel-fix2 2024-07-13 10:44:07 +03:00
98461b8d09 vercel-fix 2024-07-13 10:34:21 +03:00
0192acb8a4 author-page-view 2024-07-13 10:02:05 +03:00
b7e775eeea author-page-wip 2024-07-13 10:01:41 +03:00
25d217389b sass-depr-suppres 2024-07-12 19:00:30 +03:00
e68741efa1 stylelint-order-fmt 2024-07-12 16:19:49 +03:00
3041ee2fd6 some-reverts 2024-07-12 15:10:22 +03:00
41f989024c lock-fix 2024-07-12 14:38:54 +03:00
c2b56ed745 biomed2 2024-07-12 14:20:31 +03:00
7623f719a5 biomed 2024-07-12 14:19:58 +03:00
65798a7f60 Merge branch 'router-upgrade' of github.com:Discours/discoursio-webapp into router-upgrade 2024-07-12 14:18:28 +03:00
27653a4db2 stylelint-fix 2024-07-12 14:18:10 +03:00
Stepan Vladovskiy
d9a0badedd feat: app.tsx now with AutorsPaginator wrap, AllAuthors with logs and some dont needed changes, working on it. authors witrh minor changes 2024-07-12 06:46:12 +00:00
Stepan Vladovskiy
b677cb3493 feat: step to all authors, with debug level an dsome minor changes in memo 2024-07-11 23:00:00 +00:00
Stepan Vladovskiy
59885486eb feat: local server starts on https://localhost = true 2024-07-09 20:53:28 +00:00
edbd7ec3b2 :topic-page-fix
Some checks failed
deploy / test (push) Failing after 6m0s
deploy / Update templates on Mailgun (push) Has been skipped
2024-07-09 20:41:50 +03:00
481e4292b5 e2e-title-check-fix 2024-07-09 17:32:48 +03:00
6602f48693 one-article-fix 2024-07-09 17:13:36 +03:00
d255f6f0b1 one-article-fixes 2024-07-09 17:12:13 +03:00
bfc78d9df3 fmt 2024-07-09 15:27:50 +03:00
d4deef9bb6 does not provide reactions where not needed 2024-07-09 15:26:24 +03:00
ec1aacc010 404-fix 2024-07-09 14:58:38 +03:00
878f0036ab styles+error-new-fix 2024-07-09 13:35:57 +03:00
0c000b8b6e fmt 2024-07-09 12:57:40 +03:00
7e3499fbb3 all-topics-styling-fix 2024-07-09 12:56:56 +03:00
e3ac3cc406 meta-refactored 2024-07-09 12:13:13 +03:00
b204204a31 prep 2024-07-08 17:51:55 +03:00
327cd514d6 Merge branch 'router-upgrade' of github.com:Discours/discoursio-webapp into router-upgrade 2024-07-08 17:41:29 +03:00
0abb85ee28 fix-translation 2024-07-08 17:41:04 +03:00
Stepan Vladovskiy
c6816b9271 feat: With e2e tests on CI github worflow. README+. After tuned page need to move auth tests to main test dir 2024-07-08 10:37:56 +00:00
Stepan Vladovskiy
3bfa4adf68 fix: playwright config with baseURL env bar. 2024-07-08 10:24:07 +00:00
Stepan Vladovskiy
32519d5ee9 fix: without auth playwright tests, they are in tests-with-auth dir. testing 2024-07-08 10:10:35 +00:00
Stepan Vladovskiy
9686619243 feat: e2e_tests job is started in case if ci all jobs are successeful. In output return github.event.depolyment ststus for debug. (not a prod mode) 2024-07-08 09:52:32 +00:00
e7bcb4c6d4 fmt 2024-07-07 17:07:11 +03:00
f4f4e80816 authors-all-fix+slug-404 2024-07-07 16:48:53 +03:00
d64f68579c window-fix2 2024-07-06 09:51:25 +03:00
e627bae06c window-fix 2024-07-06 09:41:16 +03:00
fa79a0cd5d localstorage-fix 2024-07-06 09:24:37 +03:00
47622f996b lsusage-fix 2024-07-06 04:35:03 +03:00
c623356893 editpage-fix 2024-07-06 04:29:59 +03:00
90691aa650 profile-view-refactored 2024-07-06 04:25:10 +03:00
6d3629592a vercel-asks 2024-07-06 04:08:16 +03:00
d8e9dd62ce typofix 2024-07-06 04:04:55 +03:00
6ba51ad83e showup-2 2024-07-06 04:03:00 +03:00
2d89f62864 showup 2024-07-06 03:59:01 +03:00
6104079f09 fmt 2024-07-06 01:46:07 +03:00
0323c913b3 home-fetch-all-topics 2024-07-06 01:45:42 +03:00
35f39da99e topics-render-fix2 2024-07-06 01:24:56 +03:00
2d5e9877ee topics-render-fix 2024-07-06 01:24:22 +03:00
de29d435ec intl-rev-2 2024-07-06 00:40:33 +03:00
6fc2d107e9 intl-improve 2024-07-06 00:09:56 +03:00
28c66a564a preprocess-text-fix 2024-07-05 22:40:54 +03:00
a5d15f2808 router fixes 2024-07-05 20:23:07 +03:00
587c2a961a .. 2024-07-05 18:10:19 +03:00
7a499ae2e5 404-route 2024-07-05 17:22:49 +03:00
7c2d97053b utils-refactored 2024-07-05 17:08:12 +03:00
546d1bd743 minor 2024-07-05 16:37:13 +03:00
a767ce7fd1 fmt 2024-07-05 16:35:13 +03:00
bff5de0c8e lang-fix 2024-07-05 16:34:19 +03:00
22f45d8bd9 fmt 2024-07-05 11:12:17 +03:00
3433ba0aac page-titles 2024-07-05 11:11:57 +03:00
6354bb8d47 footer-translation-fix 2024-07-05 10:38:53 +03:00
cc60bb99ce fmt 2024-07-05 10:32:15 +03:00
545c25d305 Merge branch 'router-upgrade' of github.com:Discours/discoursio-webapp into router-upgrade 2024-07-05 10:26:07 +03:00
b5c63fbbb0 .. 2024-07-05 10:26:02 +03:00
e83fd35b85 static-routes-fixes 2024-07-05 10:23:59 +03:00
Stepan Vladovskiy
35e282fb18 fix: merge github workflows in one file, trying github deploy status triger, drink tea and eat cookies 2024-07-04 21:36:20 +00:00
Stepan Vladovskiy
a7e1a1763c feat: AGAIN changed metology of starting e2e tests, now after deployment-status is changed and url is active. No more certs in tests dir, aliluya, and all for github only. Gitea CI was made not by dufok, so let it be 2024-07-04 21:12:34 +00:00
Stepan Vladovskiy
7383433b58 feat: changed metology of starting e2e tests, now after deployment-status is changed and url is active. No more certs in tests dir, aliluya, and all for github only. Gitea CI was made not by dufok, so let it be 2024-07-04 21:09:15 +00:00
Stepan Vladovskiy
7d921cee2f feat: e2e tests are tuned for using CI script not only with localhost. But CI logic ssr not used in CI, need to generate frontend 2024-07-04 12:02:36 +00:00
874654f0eb merged 2024-07-04 10:52:37 +03:00
8b773e5fed refactored-folder-structure+imports-alias 2024-07-04 10:51:15 +03:00
e5950417ea lock-upgrade 2024-07-04 10:33:11 +03:00
Stepan Vladovskiy
82e8d0d00e fix: change output build directory to output/public 2024-07-04 01:22:10 +00:00
Stepan Vladovskiy
1cac091a1c style: refactor tests code to make it clean. But maybe this is can solve the problem in 404 2024-07-04 01:11:10 +00:00
Stepan Vladovskiy
fd4233f907 fix: in package var BASE url for e2e tests ci is correted to https 2024-07-04 00:58:40 +00:00
Stepan Vladovskiy
d321cb21b7 fix: add certs 2024-07-04 00:54:33 +00:00
Stepan Vladovskiy
105b286b37 fix: crzy dev, not use this, certs in tests/certs dir for https deploy e2e server 2024-07-04 00:50:41 +00:00
Stepan Vladovskiy
49d6f17779 feat: playwright tests withability tio test running or not server not lonly in localhost mode 2024-07-04 00:29:23 +00:00
Stepan Vladovskiy
bd4aa9c0ac feat: if start e2e script is has CI var set to any, than playwright not checks local http is run and dont truing to run local dev on 2024-07-04 00:17:48 +00:00
Stepan Vladovskiy
272d9caa67 fix: default value changed to actual ip:port config in packges 2024-07-03 23:49:30 +00:00
Stepan Vladovskiy
f7294c96d1 fix: separated e2e tests in mode of local testing and CI testing 2024-07-03 23:46:46 +00:00
Stepan Vladovskiy
b36983b583 fix: with not global http-serv install. Fingercross and last try' 2024-07-03 23:38:53 +00:00
Stepan Vladovskiy
0e5cc90648 fix: with http-server start on github actions 2024-07-03 23:33:49 +00:00
Stepan Vladovskiy
24f4a22110 fix: need to deploy before e2e. Right now without e2e 2024-07-03 23:18:21 +00:00
Stepan Vladovskiy
9106f45f53 feat: with e2e tests in github workflow node-ci 2024-07-03 22:48:37 +00:00
3e8efdcae5 e2e-merged 2024-07-04 01:33:50 +03:00
0a997bdcae Merge branch 'feature/e2e-tests' into router-upgrade 2024-07-04 01:25:07 +03:00
f7b39687da import-fix 2024-07-04 00:39:29 +03:00
d8144c4a4d fmt 2024-07-04 00:35:53 +03:00
b909f83411 links-minor-fixes 2024-07-04 00:35:34 +03:00
a2514735f9 links-fix 2024-07-04 00:31:39 +03:00
0f8744dc5d thanks-page+route-fix 2024-07-04 00:27:58 +03:00
c9a8c1aa8e routing-structure-fin 2024-07-04 00:25:03 +03:00
3fa235cd16 feed-wip 2024-07-03 20:38:43 +03:00
020768f5b1 guide-static 2024-07-03 10:23:36 +03:00
5711b26644 navheader-fix 2024-07-03 10:03:48 +03:00
e1275b76ea navheader-fix 2024-07-03 10:02:46 +03:00
e4390d83e6 header-links-fix 2024-07-01 18:30:45 +03:00
d2771ac2a4 vercel-adapt 2024-07-01 16:57:30 +03:00
c8ab5ef4f6 solid-start-deploy 2024-07-01 16:57:03 +03:00
2f2c4af161 Merge branch 'router-upgrade' into feature/rating 2024-07-01 16:54:31 +03:00
f55fad3d76 deploy.sh 2024-07-01 16:54:01 +03:00
c1d6b4498a Merge branch 'router-upgrade' into feature/rating 2024-07-01 16:46:49 +03:00
b855e1c11f all-authors-dummy 2024-07-01 16:41:22 +03:00
a8d7a28297 draft abstract topic slug page 2024-07-01 16:27:45 +03:00
deeb7c58cf linted 2024-06-28 18:26:00 +03:00
4e631f5f91 home-fix+topics-wip 2024-06-28 18:05:45 +03:00
a616f97fe4 topics-context-fix 2024-06-28 14:45:25 +03:00
ed145ad447 undouble-main-layout-2 2024-06-28 13:48:11 +03:00
c0c379a918 undouble-main-layout 2024-06-28 13:45:44 +03:00
6837bd3d48 pkg-bump 2024-06-28 13:35:03 +03:00
305bd23f9f home-tops 2024-06-28 10:47:38 +03:00
ae207a02f2 preload-only-body 2024-06-26 14:42:35 +03:00
fcc7d19f59 search-fix2 2024-06-26 11:25:37 +03:00
2bc87b5e99 search-fix 2024-06-26 11:23:21 +03:00
e38fce68c6 ga-integration 2024-06-26 11:22:05 +03:00
a5eaeab5cd langswitch-fix 2024-06-26 01:52:46 +03:00
4c7839aaff apiurl-fix+modal-fmt 2024-06-25 22:56:42 +03:00
c6ae893403 modal-fix, media-query-fix 2024-06-25 22:10:00 +03:00
1d38c12509 connect-page 2024-06-25 20:36:45 +03:00
81a85f7da7 styles-fix 2024-06-25 19:43:02 +03:00
0a193d340a footer-loc-fix 2024-06-25 18:30:45 +03:00
35ab9abe06 Merge branch 'feature/e2e-tests' of github.com:Discours/discoursio-webapp into feature/e2e-tests 2024-06-25 18:17:07 +03:00
9eb30da07c Merge branch 'dev' into feature/e2e-tests 2024-06-25 18:16:26 +03:00
d002a494e8 ftr-fix 2024-06-25 18:14:48 +03:00
71e38d233a fns-back 2024-06-25 17:32:32 +03:00
1f326c7611 try-no-api 2024-06-25 17:22:28 +03:00
876e342f4c edge-try 2024-06-25 17:17:02 +03:00
7ceb9db77f ssr-fix 2024-06-25 16:47:28 +03:00
e697f041ff trig-deploy-2 2024-06-25 15:59:58 +03:00
2f73152829 trig-deploy 2024-06-25 14:52:21 +03:00
f3681a2b1b ci-lint-fix 2024-06-25 14:31:05 +03:00
bc52dcc653 tsc-ok 2024-06-25 14:25:20 +03:00
bbff52019b lockfix-2 2024-06-25 14:09:26 +03:00
7591c3d40f pkgcmd 2024-06-24 22:16:49 +03:00
b2bf8335ca engine 2024-06-24 21:57:24 +03:00
4043b83aca addlock 2024-06-24 21:03:28 +03:00
8d39c74242 gigantic-wip 2024-06-24 20:50:27 +03:00
Stepan Vladovskiy
c1bd614845 style: Readme with worker 1 info 2024-06-19 01:10:27 +00:00
Stepan Vladovskiy
d5e95eb7bd feat: added in gitignore devcontainers 2024-06-19 01:06:59 +00:00
Stepan Vladovskiy
94520bc57b feat: Playwright now can start all from comand line, tests are placed in separate files for use workers, webServer is starts before all workers 2024-06-19 01:06:34 +00:00
Stepan Vladovskiy
6fcd0105de feat: with two script now, instead of one. e2e 2024-06-19 01:03:31 +00:00
Stepan Vladovskiy
250947a8d1 style: with info in README 2024-06-19 01:02:05 +00:00
3e214d0352 nosentry-hotfix-2
Some checks failed
deploy / test (push) Failing after 3m39s
deploy / Update templates on Mailgun (push) Has been skipped
2024-06-17 17:23:49 +03:00
4901a761b3 nosentry-hotfix 2024-06-17 17:23:21 +03:00
9c530c4180 .. 2024-06-17 14:45:58 +03:00
Stepan Vladovskiy
86c091f366 feat: With auth e2e test scenarios, not fully done. With debug mode playwright in main yml for gitea 2024-06-13 08:38:43 -03:00
914f9a6a7b bad-creds-show 2024-06-11 23:06:02 +03:00
8b5b5a609f posts-string-fix 2024-06-09 12:36:47 +03:00
95232f8358 meta-context-fix+counter-strings-fix 2024-06-09 12:08:59 +03:00
7fc67c07a4 client-id-session 2024-06-09 08:58:09 +03:00
Tony
ab61c1e35a
Merge pull request #466 from Discours/hotfix/following
hotfix following status update
2024-06-06 17:50:01 +03:00
2a3fe3b89e Merge branch 'dev' into hotfix/following 2024-06-06 17:46:35 +03:00
Tony
1ce6c9cd63
Merge pull request #463 from Discours/hotfix/expo
Expo fixes
2024-06-06 17:42:27 +03:00
dog
e40df167d8 fix all expo path 2024-06-06 13:25:39 +03:00
1a2d197e0c profile-hotfix 2024-06-06 13:20:10 +03:00
9ba7bfada8 Fixed code style 2024-06-06 13:20:10 +03:00
0e8e8f5d20 Add icons to the feed context popup 2024-06-06 13:20:10 +03:00
fa54d89d53 Table of contents minor fixes 2024-06-06 13:20:10 +03:00
fd76d98d96 editor-autosave-fix 2024-06-06 13:20:10 +03:00
f288cc3388 multierror 2024-06-06 13:20:10 +03:00
2952e15591 login-validations-fixes 2024-06-06 13:20:10 +03:00
8a36b21e6e handle-auth-errors 2024-06-06 13:20:10 +03:00
2cbd54d440 defer-fix 2024-06-06 13:15:54 +03:00
fc8d48436a topic-profile-fixed 2024-06-06 12:27:49 +03:00
c8ebb699e2 render-sync 2024-06-06 12:04:01 +03:00
89a55f7d15 topic-fix 2024-06-06 11:36:07 +03:00
f1b705633c minor 2024-06-06 11:01:56 +03:00
121a71873b domain-fix 2024-06-06 10:59:36 +03:00
9dd9bc7cab debug-session 2024-06-06 10:06:36 +03:00
85e21b0fc3 domains-prepare 2024-06-06 09:59:10 +03:00
79d8835c89 subscounter-fix 2024-06-06 09:34:30 +03:00
d338510454 subscounter-fix 2024-06-06 09:32:35 +03:00
c4be770375 ci-fix-2 2024-06-06 09:18:23 +03:00
e286aee46c ci-fix 2024-06-06 09:16:45 +03:00
4e71a2e748 try-e2e 2024-06-06 09:14:48 +03:00
3ec7420eb8 webkit2e2 2024-06-06 09:10:36 +03:00
d249cb050d e2e tests fine 2024-06-06 09:05:23 +03:00
cada98f135 refactoring: subscribe-term separated to newsletter and following 2024-06-06 08:44:59 +03:00
35cb6c1a44 Merge branch 'fix/topic-header' into hotfix/following 2024-06-06 07:57:18 +03:00
6b0fa86c21 Merge branch 'fix/topic-header' of github.com:Discours/discoursio-webapp into fix/topic-header 2024-06-06 07:56:04 +03:00
48a8e88b15 linted 2024-06-05 20:54:56 +03:00
823ff181c6 Merge branch 'feature/empty-feed' into hotfix/following 2024-06-05 20:49:31 +03:00
2ff29b9e0f profile-fix 2024-06-05 19:31:31 +03:00
603ebbb4a5 session-patch 2024-06-05 19:11:48 +03:00
409e64ddaf lock 2024-06-05 18:11:09 +03:00
2a80dce98a splice-stab-patch 2024-06-02 13:37:54 +03:00
b9705ab8ba merged-empty-header 2024-05-31 15:18:14 +03:00
e6a4db2eb5 Merge remote-tracking branch 'hub/fix/topic-header' into hotfix/following 2024-05-30 21:58:52 +03:00
4fe0d8e841 linted-fix 2024-05-30 21:35:51 +03:00
83656fedb5 warnfix 2024-05-30 21:33:54 +03:00
aa0fc4ad95 About-fix 2024-05-30 13:37:14 +03:00
fc25522516 profile-hotfix 2024-05-27 22:13:55 +03:00
7ba6bb2f97 Merge branch 'feature/empty-feed' of https://github.com/Discours/discoursio-webapp into feature/empty-feed 2024-05-25 19:58:23 +03:00
3f8f3492c8 Fixed placeholder image size 2024-05-25 19:57:14 +03:00
309b07c596 Fixed code style 2024-05-25 19:35:02 +03:00
65d7645374 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into feature/empty-feed 2024-05-25 19:30:35 +03:00
3319bfe973 Fixed subscribers style 2024-05-25 19:27:15 +03:00
8caef5b1f0 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/topic-header 2024-05-24 21:38:33 +03:00
4e7cfa9241 profile-fixes 2024-05-24 18:07:27 +03:00
ac9fd8d313 Merge branch 'dev' into feature/empty-feed 2024-05-24 18:00:41 +03:00
5f194c7a3b Merge branch 'feature/empty-feed' of github.com:Discours/discoursio-webapp into feature/empty-feed 2024-05-24 18:00:30 +03:00
b7ce071ee9 Merge branch 'dev' of github.com:Discours/discoursio-webapp into hotfix/following 2024-05-24 17:59:29 +03:00
9a42086f08 auth-minor-fixes 2024-05-24 17:59:15 +03:00
Tony
ba6d29ef59
Merge pull request #474 from Discours/hotfix/context-popup-icons
Add icons to the feed context popup
2024-05-22 14:23:31 +03:00
b53aa85337 hardcoded-config-fix 2024-05-21 16:51:36 +03:00
144db5c0e8 hardcoded-config 2024-05-21 16:51:13 +03:00
36ca02dc1b hardcoded-apiurl 2024-05-21 14:13:37 +03:00
e327238678 result-fix6 2024-05-21 04:49:49 +03:00
7b546ecbc8 stab 2024-05-21 04:20:08 +03:00
46b80a3182 rlbk 2024-05-21 04:11:47 +03:00
068fadd19b testdomains 2024-05-21 04:10:34 +03:00
0b01f41edc redeploy 2024-05-21 04:03:29 +03:00
79f94876f0 subs-refactoring 2024-05-21 02:15:52 +03:00
be4d16b1a5 Merge branch 'fix/topic-header' of https://github.com/Discours/discoursio-webapp into fix/topic-header 2024-05-21 00:36:00 +03:00
27d3496423 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/topic-header 2024-05-21 00:31:38 +03:00
2e6e1abad7 Fixed banners style 2024-05-21 00:26:55 +03:00
0631ecf8c9 Fixed line breaks 2024-05-20 23:51:33 +03:00
417c57e338 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into feature/empty-feed 2024-05-20 23:41:56 +03:00
8397facc5e Fixed code style 2024-05-20 23:39:59 +03:00
5bde0bc7d0 Add icons to the feed context popup 2024-05-20 23:37:56 +03:00
bec333f7c3 packaging-upgrade 2024-05-20 18:53:48 +03:00
652d0b647a refactoring:following 2024-05-20 14:16:54 +03:00
a25d50d99b sorted, subscribe -> follow 2024-05-20 13:48:38 +03:00
0a5e5eca95 tiptap deprecated warning fix 2024-05-20 11:31:55 +03:00
58ec520c3e Merge branch 'dev' into hotfix/following 2024-05-20 11:17:06 +03:00
Tony
fe2db946b4
Merge pull request #473 from Discours/hotfix/contents
Table of contents minor fixes
2024-05-20 10:51:04 +03:00
9b76a52430 editor-autosave-fix 2024-05-19 02:22:19 +03:00
8f330ab914 multierror 2024-05-19 01:41:50 +03:00
319136474e Table of contents minor fixes 2024-05-19 01:38:56 +03:00
7e2f2d5192 Fixed article cover animation 2024-05-19 01:26:07 +03:00
38899ad8cb login-validations-fixes 2024-05-19 01:18:55 +03:00
59eaf3837d handle-auth-errors 2024-05-19 01:14:28 +03:00
e4f7675606 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into hotfix/expo 2024-05-19 01:04:49 +03:00
22f0c9052d auth-errors-fix 2024-05-19 01:04:18 +03:00
f22d10a535 Placeholders fixes 2024-05-19 01:03:06 +03:00
135e0d215f error-catch 2024-05-19 00:55:30 +03:00
4a55271a79 edit-hotfix 2024-05-19 00:48:58 +03:00
18a23ee0f8 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into feature/empty-feed 2024-05-18 23:54:08 +03:00
60664581bd logfix 2024-05-18 20:50:27 +03:00
95c4d777b2 connect-fox 2024-05-18 20:43:20 +03:00
c8517c85c6 edit-hotfix 2024-05-18 20:36:22 +03:00
18bd07291d edit-effect 2024-05-18 20:36:06 +03:00
0b06e41670 authors added 2024-05-18 20:16:45 +03:00
af806590fb postmerge 2024-05-18 19:55:24 +03:00
2e82eec82c Merge branch 'feature/empty-feed' into fix/topic-header 2024-05-18 19:47:01 +03:00
74f7469c7d topic followers + shouts counter 2024-05-18 19:45:36 +03:00
0ecbf07ef6 Merge branch 'dev' into feature/empty-feed 2024-05-18 17:45:51 +03:00
40b2c80b54 Merge branch 'dev' into fix/topic-header 2024-05-18 17:17:22 +03:00
Tony
6c99aa1adf
Merge pull request #469 from Discours/fix/all-topics-page
Fix/all topics page
2024-05-18 17:15:37 +03:00
de7afc2691 Merge branch 'dev' into fix/all-topics-page 2024-05-18 17:13:04 +03:00
Tony
8adff4629f
Merge pull request #468 from Discours/fix/popups
Enlarge profile popup
2024-05-18 17:09:44 +03:00
f14ba82a29 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into feature/empty-feed 2024-05-18 17:07:53 +03:00
Tony
2c5ba6edab
Merge pull request #470 from Discours/hotfix/header-navigation-style
Hotfix/header navigation style
2024-05-18 17:04:22 +03:00
Tony
0fa881dd6e
Merge pull request #471 from Discours/hotfix/scroll-to-comments
Hotfix/scroll to comments
2024-05-18 17:02:24 +03:00
412360f87f Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into hotfix/scroll-to-comments 2024-05-18 16:59:50 +03:00
39d555c490 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into hotfix/header-navigation-style
 Conflicts:
	src/components/Nav/HeaderAuth.tsx
2024-05-18 16:58:10 +03:00
Tony
e16f20db81
Merge pull request #467 from Discours/hotfix/snackbar
Fixed snackbar position
2024-05-18 16:52:01 +03:00
c61ad86234 sse 2024-05-18 16:44:57 +03:00
3f96850948 connect-logs-fix 2024-05-18 16:44:57 +03:00
150903ecbd update-token-fix 2024-05-18 16:44:57 +03:00
aa8d5973ee swiper-fix 2024-05-18 16:44:57 +03:00
ed2b4ebfbf stab-hotfix 2024-05-18 16:44:57 +03:00
Ilya Y
cb5c78790b Feature/profile settings page (#452)
* Init change password form
2024-05-18 16:44:57 +03:00
0bb4465e87 Merge branch 'dev' into hotfix/following 2024-05-18 16:38:16 +03:00
f98bfa2d39 Merge branch 'dev' into feature/rating
All checks were successful
deploy / test (push) Successful in 2m56s
deploy / Update templates on Mailgun (push) Has been skipped
2024-05-18 16:35:41 +03:00
6a83aa6f87 sse 2024-05-18 15:47:37 +03:00
91d6e0d41b connect-logs-fix 2024-05-18 15:46:51 +03:00
65b0667a26 logs-connect-fix 2024-05-18 15:46:29 +03:00
ae90118045 session-add 2024-05-18 15:13:43 +03:00
e7f17c3cc9 Merge branch 'dev' into hotfix/following 2024-05-18 15:10:30 +03:00
909c9a2983 Merge branch 'dev' of github.com:Discours/discoursio-webapp into feature/rating 2024-05-18 15:01:43 +03:00
07f2770a98 update-token-fix 2024-05-18 14:25:37 +03:00
abec4043a3 swiper-fix 2024-05-18 13:45:35 +03:00
50387738f8 stab-hotfix 2024-05-18 13:44:43 +03:00
5bbd29ddd2 Fixed style on hovered article card 2024-05-17 23:45:53 +03:00
49ff3ccbe3 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into hotfix/expo 2024-05-17 23:28:57 +03:00
c261c8cad0 nopublic 2024-05-16 17:22:26 +03:00
Ilya Y
a9f732d1a4
Feature/profile settings page (#452)
* Init change password form
2024-05-13 02:36:46 +03:00
399deaec86 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into feature/empty-feed 2024-05-11 20:34:47 +03:00
7d69c55963 Code style fixes 2024-05-11 20:33:40 +03:00
0664b5c933 Placeholders on the user profile page 2024-05-11 20:27:57 +03:00
79749bd95e about-hotfix 2024-05-11 19:11:34 +03:00
2334c6b58c Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-05-11 17:55:05 +03:00
bc6b35c374 hotifx-profile 2024-05-11 17:54:40 +03:00
90c47a0177 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/topic-header 2024-05-10 20:17:59 +03:00
9542fd0209 Fixes props type in the Placeholder.tsx 2024-05-10 20:14:51 +03:00
be7e31dbd2 Feed page fixes 2024-05-10 20:09:37 +03:00
e53181ef2d Links in the placeholders for authorized users 2024-05-10 19:57:34 +03:00
6a789d4a0e Feed placeholders 2024-05-10 19:52:13 +03:00
Tony
aebff60263
Merge pull request #460 from Discours/hotfix/author-profile
Hotfix/author profile
2024-05-10 17:41:19 +03:00
9ec36168ae Topic header fixes 2024-05-10 17:14:06 +03:00
1e33d18c54 Expo fixes 2024-05-10 15:59:21 +03:00
ab7dda6c14 safari-regexp-fix 2024-05-10 15:44:36 +03:00
53d3e2d836 nounswiped 2024-05-09 18:08:54 +03:00
7d24fe5598
Fixed snackbar position (#461) 2024-05-09 14:26:48 +03:00
Kosta
30672fc1af
Enlarge profile popup (#462)
Co-authored-by: kvakazyambra <kvakazyambra@gmail.com>
2024-05-09 14:26:16 +03:00
7d490c91aa Fixed snackbar position 2024-05-09 12:15:39 +03:00
2fa5a98933 Enlarge profile popup 2024-05-09 12:04:55 +03:00
605dae2127 common-result 2024-05-08 23:43:38 +03:00
7ffb568661 author-debug 2024-05-08 00:12:53 +03:00
08533f4d1f debug-author-profile 2024-05-08 00:10:54 +03:00
Tony
138403dfc3
Merge pull request #459 from Discours/hotfix/topic-reopen
Hotfix/topic reopen
2024-05-07 19:22:56 +03:00
67004a889f typed-2 2024-05-07 18:59:49 +03:00
0b8ee33ed5 typed 2024-05-07 18:56:46 +03:00
c52e79faf0 import-typo-fix 2024-05-07 18:49:39 +03:00
9c1ed4a04b orig-meta 2024-05-07 18:38:03 +03:00
ec0c2cf136 isorted-fmt 2024-05-07 18:17:31 +03:00
f992cf9377 my-meta 2024-05-07 18:05:22 +03:00
f6043ad223 lock-fix 2024-05-07 16:47:03 +03:00
e1a69d97c2 vike-downgrade 2024-05-07 16:16:43 +03:00
8df9e3c356 bump-ver 2024-05-07 15:59:07 +03:00
78210d558f seen-usecontext 2024-05-07 11:51:17 +03:00
a03c26dd5a seen-fix 2024-05-07 11:15:20 +03:00
1f3b52258d topics renew every hour 2024-05-07 03:05:04 +03:00
56b292c817 refactoring: topics context provider 2024-05-07 02:44:25 +03:00
a885686ae4 stab 2024-05-07 02:44:06 +03:00
06ce5266e2 refactoring: seen context provider 2024-05-07 02:43:23 +03:00
a75401b802 swiper-1-2-fix 2024-05-07 02:32:49 +03:00
Tony
73b42dbf09
Merge pull request #457 from Discours/fix/popups
Unified popups style
2024-05-07 01:37:31 +03:00
1aa1dd3648 Remove redundant params for popup 2024-05-07 01:16:28 +03:00
8ea55b3632 header-hotfix 2024-05-07 01:15:57 +03:00
b7f19353f3 Fixed code style 2024-05-07 01:13:16 +03:00
ce86896c1d Unified popups style 2024-05-07 01:01:20 +03:00
52ef17dc6d hotfix-onecard-swiper 2024-05-07 00:51:07 +03:00
75c415aece Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-05-06 23:47:04 +03:00
e6888d2549 hotifx-dx 2024-05-06 23:45:54 +03:00
8fc86b9bd9 linted 2024-05-06 23:36:27 +03:00
417af2fc20 comment-rating-rebased 2024-05-06 23:26:16 +03:00
7d515c4fe2 Merge branch 'dev' of github.com:Discours/discoursio-webapp into feature/rating 2024-05-06 23:22:29 +03:00
Tony
94a9eb4fff
Merge pull request #456 from Discours/hotfix/comments-renew
load-after-create
2024-05-06 22:41:51 +03:00
502dcfae6d is-posting-comment-feature 2024-05-06 21:45:17 +03:00
dcbeb55ac9 draft-permission-error-msg 2024-05-06 21:37:54 +03:00
3ce53728ea drafts-reload-session 2024-05-06 21:16:13 +03:00
6e8b6043d7 Merge branch 'dev' of github.com:Discours/discoursio-webapp into hotfix/comments-renew 2024-05-06 19:20:59 +03:00
3fadd719b2 load-after-create 2024-05-06 19:17:34 +03:00
Tony
764ad4ea08
Merge pull request #455 from Discours/hotfix/header
Hotfix/header
2024-05-06 19:00:08 +03:00
d8f61e5b66 styles-fixme 2024-05-06 18:43:10 +03:00
500e8132dd fix nav header 2024-05-06 18:39:01 +03:00
Tony
a024080661
Merge pull request #436 from Discours/feature/glitchtip
error collector connected
2024-05-06 17:46:15 +03:00
1c02d3f29b login-error-fix 2024-05-06 17:42:18 +03:00
75c7ef00f6 Merge branch 'dev' into feature/glitchtip 2024-05-06 17:36:20 +03:00
Tony
8f09d6fc54
Merge pull request #447 from Discours/hotfix/editor-permission
use-session in editor
2024-05-06 17:33:04 +03:00
fe695f427f postmerge 2024-05-06 17:26:03 +03:00
82e2abf8e3 Merge branch 'dev' into hotfix/editor-permission 2024-05-06 17:23:24 +03:00
Tony
afd9bba005
Merge pull request #454 from Discours/hotfix/uploader
Hotfix/uploader
2024-05-06 17:16:57 +03:00
Tony
1c61692802
Merge pull request #453 from Discours/hotfix/linted
Hotfix/linted
2024-05-06 14:53:32 +03:00
8e69c3979e fmt 2024-05-06 14:08:41 +03:00
83379c53ae auth-upload 2024-05-06 14:07:19 +03:00
56f46c18dd shuffle-topic-fix-3 2024-05-06 13:53:35 +03:00
3f8d495076 shuffle-topic-fix-2 2024-05-06 13:50:15 +03:00
28022607e3 topics-fix-2 2024-05-06 13:46:00 +03:00
1b1f3441dd shuffle-tolerate 2024-05-06 13:33:57 +03:00
0cbd3aedba following-debug 2024-05-05 20:04:47 +03:00
9a475b8d0c Merge branch 'hotfix/linted' into feature/rating 2024-05-05 19:32:25 +03:00
eb17b5185f Merge branch 'hotfix/linted' into feature/glitchtip 2024-05-05 19:30:45 +03:00
7d17d63b5d buffer-fix 2024-05-05 19:20:49 +03:00
26b7afae66 vite-fix 2024-05-05 19:19:14 +03:00
be9a4ff275 fmt 2024-05-05 19:17:06 +03:00
39b15320b7 isolated-type 2024-05-05 19:15:56 +03:00
fa78483a38 biomed 2024-05-05 19:13:48 +03:00
629c2ad3de fmt 2024-05-04 23:35:36 +03:00
3161c2b2ec fix 2024-05-04 22:56:26 +03:00
dee6cfbd34 pkg-fix 2024-05-04 22:54:33 +03:00
848d791e42 postmerge 2024-05-04 22:52:31 +03:00
193015a912 postmerge 2024-05-04 22:20:16 +03:00
9277d652e9 postmerge 2024-05-04 22:17:36 +03:00
21c216d19c lock-fix-2 2024-05-04 22:10:10 +03:00
ce670d0ff7 lock-fix 2024-05-04 20:23:51 +03:00
7cfe63b790 lock-fix 2024-05-04 20:23:28 +03:00
fbeceb820d postmegred-2 2024-05-04 20:22:29 +03:00
7af4e91792 postmerge 2024-05-04 20:21:00 +03:00
9f6e2c7893 postmerge 2024-05-04 20:18:08 +03:00
b36e39edf0 postmerge 2024-05-04 20:17:47 +03:00
b1cd3d917b Merge remote-tracking branch 'hub/dev' into feature/glitchtip 2024-05-04 14:57:33 +03:00
b4d6171cea Merge remote-tracking branch 'hub/dev' into feature/rating 2024-05-04 14:54:06 +03:00
df90953138 Merge branch 'hotfix/editor-permission' into feature/rating 2024-05-04 14:52:24 +03:00
27a9662143 Merge branch 'hotfix/editor-permission' into feature/glitchtip 2024-05-04 14:51:13 +03:00
39dd78e25d checkfix 2024-05-04 14:39:05 +03:00
95fb194a96 condition0fix 2024-05-04 14:33:14 +03:00
3835618f3a save-draft-fix 2024-05-04 14:31:52 +03:00
439be16fa1 draft-save-debug 2024-05-03 16:38:12 +03:00
6aa84c17be
Fix/all topics page (#432)
* Fixed topics page

* linted

* Topics list fixes

* Revert styles for user descriptions

* Fixed author badge in the following modal

---------

Co-authored-by: ilya-bkv <i.yablokov@ccmp.me>
2024-05-03 11:49:05 +03:00
ad2f6be1ae
Fixed header icons (#451)
* Fixed header icons

* Code style fixes
2024-05-03 11:48:49 +03:00
7dc2efca66 isAuthenticate memo removed 2024-05-03 11:36:15 +03:00
e0ed344218 fmt 2024-05-03 01:43:13 +03:00
b7939dead0 saving-draft-fix 2024-05-03 01:40:33 +03:00
bd0fdeeb1f saveDraft-fix 2024-05-03 01:39:28 +03:00
3bb0044f6e Code style fixes 2024-05-03 00:06:03 +03:00
f0b2ef0ef9 Fixed header icons 2024-05-03 00:02:48 +03:00
a89a9bb1f4 Merge branch 'hotfix/editor-permission' into feature/rating 2024-05-02 17:05:46 +03:00
571e475445 Merge branch 'dev' into feature/rating 2024-05-02 17:05:31 +03:00
ilya-bkv
004baba591 Merge remote-tracking branch 'origin/fix/all-topics-page' into fix/all-topics-page 2024-05-02 17:02:09 +03:00
ilya-bkv
3539ab500e Merge remote-tracking branch 'origin/dev' into fix/all-topics-page 2024-05-02 16:58:34 +03:00
e6ff073e29 buffer-poly-fix 2024-05-02 01:27:34 +03:00
2500206a9d vite-poly 2024-05-02 01:24:03 +03:00
7896950ec8 nodeversion 2024-05-01 23:07:46 +03:00
3075626d57 buffer-fix-2 2024-05-01 22:05:15 +03:00
f77f0f08eb sessionfix+bufferfix 2024-05-01 21:34:35 +03:00
18b7b22270 suspense-fix 2024-05-01 20:40:40 +03:00
4b1d21b15e buffer-added 2024-05-01 18:59:38 +03:00
284c728b61 defer-fixed 2024-05-01 18:54:40 +03:00
e29188726c popup-fix 2024-05-01 18:23:47 +03:00
f02abc3ba0 lock-fix 2024-05-01 18:23:47 +03:00
702865b0e6 postmerge biomed 2024-05-01 18:23:47 +03:00
98e0bb1078 revised changes 2024-05-01 18:23:47 +03:00
Tony
ecd038a306
Merge pull request #448 from Discours/hotfix/header-navigation-style
Header navigation style fixes
2024-05-01 11:32:24 +03:00
04978ebc7c
Feed style fixes (#450)
* Feed style fixes

* Change subscriptions list icon
2024-04-30 20:22:44 +03:00
a4d6466392
Fixed scrolling to comments (#449) 2024-04-30 18:58:27 +03:00
11b932c30e Fixed scrolling to comments 2024-04-30 18:43:38 +03:00
12ca2b9a97 Fixed author badge in the following modal 2024-04-30 18:14:10 +03:00
c1a7291401 Fixed paddings in the header 2024-04-30 17:27:38 +03:00
e1484e0aa9 Revert styles for user descriptions 2024-04-30 16:56:39 +03:00
867acd4b90 Fixed code style 2024-04-30 16:26:31 +03:00
3eebc0b7ed Fixed style setting 2024-04-30 16:21:56 +03:00
c907e6ffa5 Header navigation style fixes 2024-04-30 16:16:30 +03:00
4c787fe49c Topics list fixes 2024-04-30 15:26:53 +03:00
ece5f2505f Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/all-topics-page 2024-04-26 09:19:33 +03:00
ilya-bkv
b3155c4535 fix author view 2024-04-26 06:13:23 +03:00
ilya-bkv
ac39230271 update 2024-04-26 06:05:10 +03:00
7c9ecd1e3a Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/all-topics-page 2024-04-25 23:38:04 +03:00
Tony
b9f7d01339
Merge pull request #427 from Discours/hotfix/correct-following-status
Hotfix/correct following status
2024-04-25 19:36:32 +03:00
Ilya Y
1c94638ce8
Add pagination on Expo (#441)
* Add pagination on Expo

* update Expo load articles method
2024-04-25 16:55:34 +03:00
84fb665f9d postmerge-fix-2 2024-04-25 13:55:58 +03:00
23326b10f7 postmerge-fix 2024-04-25 13:53:51 +03:00
e38d3b39b7 Merge branch 'dev' into hotfix/correct-following-status 2024-04-25 13:46:36 +03:00
3174c9d83c Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/all-topics-page 2024-04-24 22:31:11 +03:00
ilya-bkv
5334291878 fix feed dropdown 2024-04-24 15:48:37 +03:00
Ilya Y
9028618067
Add gallery description (#434)
Add gallery description
2024-04-24 12:30:17 +03:00
Ilya Y
c1d1f05edf
Hotfix/author comments render (#446)
Fix comments fetching
2024-04-24 11:08:00 +03:00
Arkadzi Rakouski
4a7a052d67
fix falsy active header (#444)
fix falsy active header
2024-04-24 11:07:07 +03:00
Tony
036443d7ee
Merge pull request #445 from Discours/feature/getImage-refactoring
getImageUrl.ts refactoring
2024-04-24 10:31:37 +03:00
ilya-bkv
fc056f14b8 fix biome 2024-04-24 10:23:42 +03:00
ilya-bkv
59561ec26b add unsafe 2024-04-24 10:22:05 +03:00
ilya-bkv
437e666b3c getImageUrl.ts refactoring 2024-04-24 09:44:01 +03:00
Ilya Y
c9f494d2c1
Hide autofill in profile settings (#442)
* hide autofill in profile settings
2024-04-22 16:59:39 +03:00
Ilya Y
36fff73af6
Hotfix/remove state params after login (#443)
* hide autofill in profilr settings

* fix Image width

* Remove state search params after login

* fix biome
2024-04-22 16:47:37 +03:00
ilya-bkv
cca858dadf fix image width 2024-04-22 16:37:47 +03:00
6030e665eb Merge branch 'dev' into feature/rating 2024-04-19 18:30:41 +03:00
Ilya Y
16e3e75381
Add hash navigation in slider (#440)
Sliders with search params
2024-04-18 16:34:07 +03:00
36970c03c4 profile-fix 2024-04-17 19:26:26 +03:00
0f28fe891a post-oauth-fix 2024-04-17 18:47:24 +03:00
69fc0ffd07 less-code 2024-04-17 16:44:53 +03:00
6fbe9603fe conditional 2024-04-16 07:08:20 +03:00
f2012bca18 load-fix 2024-04-16 07:07:36 +03:00
f7c33c167a fmt 2024-04-16 07:03:05 +03:00
bd4fedc6c7 preloaded-author-fix 2024-04-16 07:01:18 +03:00
Tony
bb7d289ff7
Merge pull request #438 from Discours/hotfix/provile-dala-load
Load user profile
2024-04-15 21:05:13 +03:00
5ddbb0dd1b biome-ignore 2024-04-15 21:02:58 +03:00
6e48e64497 -npm-run-format 2024-04-15 21:01:00 +03:00
ilya-bkv
e3a2aaf73a Load user profile 2024-04-15 15:39:34 +03:00
ilya-bkv
78cde31943 Swiper (init with promise) 2024-04-15 07:43:48 +03:00
ec58279d5c biome-action- 2024-04-10 17:16:55 +03:00
51cd05ba64 biome-action 2024-04-10 17:15:25 +03:00
0eb72b9534 gh-action-fix-2 2024-04-10 17:12:22 +03:00
36a3606557 gh-action-fix 2024-04-10 17:09:40 +03:00
fe41845271 biome-setup 2024-04-10 17:06:45 +03:00
e1cfa376c5 fmt 2024-04-10 17:02:58 +03:00
e47650fe84 biome-fix 2024-04-10 16:57:19 +03:00
d6121efeb1 vite-bump 2024-04-10 16:54:57 +03:00
306d197d74 lock 2024-04-10 16:51:19 +03:00
8b95ae2f85 lock-fix 2024-04-10 16:40:00 +03:00
ec70a078ad connected 2024-04-10 16:37:09 +03:00
Tony
66ed666b90
Merge pull request #435 from Discours/feature/session-upgrade
app-data-author
2024-04-08 22:15:12 +03:00
44b9c98ffc Merge branch 'dev' into feature/session-upgrade 2024-04-08 20:34:50 +03:00
ilya-bkv
499341baea fix syule types 2024-04-08 18:28:08 +03:00
455006f627 fmt 2024-04-08 18:19:43 +03:00
cf0214563d use-following-data-2 2024-04-08 17:48:58 +03:00
aeb42de908 use-following-data 2024-04-08 16:14:19 +03:00
79961b7f47 fdata-fix 2024-04-08 16:04:10 +03:00
adae4c0144 Merge branch 'dev' into feature/session-upgrade 2024-04-08 15:59:50 +03:00
6d12b01d56 author-type-fix 2024-04-08 15:54:01 +03:00
58c4d6eae7 app-data-author 2024-04-08 15:49:40 +03:00
6851c3af6a
Feature/header (#408)
New header
2024-04-08 14:26:20 +03:00
69340e4b87 Merge branch 'dev' into feature/rating 2024-04-08 12:53:29 +03:00
ilya-bkv
c3495ed0b3 Fil lint 2024-04-04 09:02:34 +03:00
ilya-bkv
b752357224 Fix profile settings Is Floating PanelVisible 2024-04-04 09:01:26 +03:00
ilya-bkv
b9591d7364 Cleanup code 2024-04-02 14:28:43 +03:00
ilya-bkv
d55be2505d load Random Topics on Mount 2024-04-02 14:27:56 +03:00
ilya-bkv
e93cb76a78 Disallow in robots txt 2024-04-02 06:12:29 +03:00
24cd1c54a8 Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into fix/all-topics-page 2024-04-01 23:32:43 +03:00
6d30964a8b linted 2024-04-01 23:31:16 +03:00
ilya-bkv
9d5ddcfccc Show saving status in ProfileSettings 2024-04-01 07:16:53 +03:00
ilya-bkv
d5aa083a2f Fix Topic Top Articles 2024-03-29 20:25:17 +03:00
ilya-bkv
70bc237e2c Fix Topic Top Articles 2024-03-29 20:25:07 +03:00
ilya-bkv
5e5693332c Load more fix 2024-03-29 16:53:26 +03:00
ilya-bkv
ce0c2c0f0a Fix expo types 2024-03-29 12:58:32 +03:00
ilya-bkv
bbd8ef798c Fix expo types 2024-03-29 12:47:56 +03:00
ilya-bkv
75d929efda Fix expo 2024-03-29 12:35:01 +03:00
ilya-bkv
0b88357f7c Fix expo 2024-03-29 12:30:38 +03:00
a95686d12b Fixed topics page 2024-03-27 23:10:49 +03:00
Ilya Y
78dd43a497
Fix expo article length (#431) 2024-03-27 03:54:15 +03:00
ddaed0557d
Fixed topic body render (#429)
Fixed topic body render
2024-03-26 16:57:24 +03:00
Ilya Y
b84e7f43f7
Fix reload page after foute from profile to another profile (#430) 2024-03-25 16:07:14 +03:00
e3c00cc6cd and 2024-03-22 08:29:37 +03:00
db830308e9 fix 2024-03-22 08:28:11 +03:00
0c078a7bc1 get-image-url-hotfix 2024-03-22 08:25:01 +03:00
ilya-bkv
c80b2f044a Merge dev 2024-03-21 15:48:54 +03:00
ilya-bkv
4c655509df Merge remote-tracking branch 'origin/dev' into hotfix/correct-following-status
# Conflicts:
#	src/components/Views/Author/Author.tsx
2024-03-21 15:48:36 +03:00
ilya-bkv
d4ce74b491 Hide modal after route to profile from another profile 2024-03-21 11:42:28 +03:00
ilya-bkv
97b1ec4386 Audio player fix (play by track click) 2024-03-20 14:59:07 +03:00
5a7e416700 linted 2024-03-19 14:54:40 +03:00
e33faa049e linted 2024-03-19 14:53:59 +03:00
d36603a57c Merge branch 'dev' into feature/rating 2024-03-19 14:53:03 +03:00
5f939839fb public-env-urls 2024-03-19 14:23:20 +03:00
d9b3d18e95 Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-03-19 10:15:40 +03:00
ilya-bkv
a88d200109 Biome fix 2024-03-18 14:57:55 +03:00
ilya-bkv
546d3d2659 Fix userpic upload mutation 2024-03-18 14:55:07 +03:00
ilya-bkv
96685507ea Fix response Update reaction 2024-03-18 14:25:10 +03:00
ilya-bkv
084bd29d2b Change Expo article's count 2024-03-18 14:22:10 +03:00
Ilya Y
6812ecd187
One request for random topics (#428) 2024-03-18 14:07:28 +03:00
ilya-bkv
896c180dd1 [FIX] Show reaction after submit 2024-03-18 10:40:31 +03:00
ilya-bkv
4fdd025e44 [FIX] Show more shouts 2024-03-18 10:22:12 +03:00
ilya-bkv
8d78ba2c62 fix preload author 2024-03-15 19:42:55 +03:00
ilya-bkv
dd4065036f cleanup code 2024-03-15 18:24:33 +03:00
ilya-bkv
00a0436835 cleanup code 2024-03-15 17:58:34 +03:00
ilya-bkv
d202845aab run fix checks 2024-03-15 17:57:03 +03:00
ilya-bkv
edf4400627 Change follow logic 2024-03-15 17:55:37 +03:00
ilya-bkv
5c4d605724 [WIP] 2024-03-15 15:58:22 +03:00
ilya-bkv
b44008b229 [WIP] 2024-03-14 09:22:43 +03:00
ilya-bkv
75caee909e [WIP] 2024-03-12 11:52:39 +03:00
36120abda5 validation-fix 2024-03-11 16:23:06 +03:00
ilya-bkv
96d6e6bd0c Fix login password onBlur 2024-03-11 10:41:31 +03:00
ilya-bkv
80ffc564a9 [WiP] 2024-03-11 08:20:00 +03:00
3b033be2b6 lint-fix 2024-03-07 15:52:03 +03:00
4e2152b8b0 postmerge-fix 2024-03-07 15:49:28 +03:00
cc951c305b Merge branch 'dev' into feature/rating 2024-03-07 15:38:35 +03:00
Tony
ae589e39fa
Merge pull request #414 from Discours/hotfix/posting-author
posting author fixes
2024-03-07 15:35:30 +03:00
b31d0deed4 merged 2024-03-07 15:31:09 +03:00
ilya-bkv
248d06decd cleanup code 2024-03-07 15:14:58 +03:00
ilya-bkv
2280a776b3 Improve draft saving process in EditView 2024-03-07 15:13:52 +03:00
Tony
bf9f0d9c7b
Merge branch 'dev' into hotfix/posting-author 2024-03-07 14:09:00 +03:00
db7825fab8 onmount-fix 2024-03-07 13:58:12 +03:00
0b905eb635 merged-2 2024-03-07 13:14:22 +03:00
171458a83a merged 2024-03-07 13:04:02 +03:00
a59ee6260c create-fx-fix 2024-03-07 13:03:19 +03:00
Ilya Y
1d64d97f9f
Hotfix/parse auth errors (#423)
Add fixes to login form parse errors
2024-03-07 11:07:46 +03:00
Tony
aed9952b61
Merge pull request #426 from Discours/feature/delete-comment-error
Comment delete message
2024-03-07 10:24:55 +03:00
ilya-bkv
41e40ada9b Comment delete message 2024-03-07 10:20:50 +03:00
d7f00cd962 query-fix 2024-03-06 22:16:55 +03:00
af0c7fa712 get-my-shout-fix 2024-03-06 16:01:46 +03:00
56a83dc8ba fmt 2024-03-06 15:55:33 +03:00
e252ce464b fmt 2024-03-06 15:53:51 +03:00
3e2d7416b7 merged 2024-03-06 15:37:16 +03:00
4f6169f16d merged 2024-03-06 15:36:54 +03:00
626624ddb4 topics-comments 2024-03-06 15:36:12 +03:00
Tony
4a1ad2b5af
Merge pull request #425 from Discours/hotfix/delete-reaction-in-profile
Add delete function to Comment component
2024-03-06 15:07:06 +03:00
ilya-bkv
45e8f2ba02 Refactor Comment component and improve debug logging
Implemented a delete function in the Comment component that filters out the selected comment in real time, enhancing the user experience by providing immediate feedback upon deletion. Also, refactor the authorFollows function in the GraphQL client core to use standardized string quotations for better code consistency.
2024-03-06 15:02:32 +03:00
ilya-bkv
eb03dc1d05 Add delete function to Comment component
The Comment component has been updated to include a delete function which removes the selected comment from the displayed list in real time. This update enhances user experience by providing instant feedback when a comment is deleted. Additionally, debug logging was added to authorFollows function for testing purposes.
2024-03-06 14:56:32 +03:00
136ecda3b1 topics-comments 2024-03-06 13:57:39 +03:00
57a9fe42a8 bypass-linter 2024-03-06 12:56:00 +03:00
e2c98ded5e reaction-error-handling 2024-03-06 12:49:06 +03:00
5b97ea3746 delete-reaction-fix 2024-03-06 12:04:33 +03:00
ce66874089 no-created-by-unwrap 2024-03-06 10:30:07 +03:00
449154bd1b get-shouts-drafts 2024-03-05 18:52:34 +03:00
dc719120b2 debug-update-shout 2024-03-05 18:20:47 +03:00
2d7fd38d82 get-my-shout 2024-03-05 16:44:51 +03:00
4ed15f405e access+userpic-fix 2024-03-05 16:07:14 +03:00
f8bf3d86a0 edit-access+redirect 2024-03-05 16:01:47 +03:00
9b7079def5 naming-fix 2024-03-04 18:06:40 +03:00
50e8093fee Merge branch 'hotfix/posting-author' of github.com:Discours/discoursio-webapp into hotfix/posting-author 2024-03-04 17:10:34 +03:00
0160dec607 Merge branch 'dev' of github.com:Discours/discoursio-webapp into hotfix/posting-author 2024-03-04 17:09:26 +03:00
Tony
780c3571e4
Merge pull request #424 from Discours/feature/notifier
notifications 2.0
2024-03-04 17:07:06 +03:00
ca66517d6a update-reaction-fix 2024-03-04 16:31:31 +03:00
dbec93aee1 Merge branch 'feature/notifier' into feature/rating 2024-03-04 16:29:30 +03:00
ca41467f68 merged 2024-03-04 16:28:48 +03:00
372012badf Merge branch 'dev' into feature/rating 2024-03-04 16:28:10 +03:00
ed23e1f7a8 updated-schema 2024-03-04 15:51:34 +03:00
1e27546b3e group-index-fix 2024-03-04 15:35:34 +03:00
a207aeeb44 new-api 2024-03-04 15:32:48 +03:00
ilya-bkv
4196bb0f1e Fix comment edit without refresh (update editor state) 2024-03-04 15:18:24 +03:00
ilya-bkv
bc1ea82127 Fix comment edit without refresh 2024-03-04 13:47:11 +03:00
ilya-bkv
e0503f593f Remove comment edited date 2024-03-04 11:54:09 +03:00
96f72f00ee reactions-context-fix 2024-03-03 20:06:58 +03:00
f0bddfe461 rating-update-fix 2024-03-03 19:39:25 +03:00
1397cc9b84 fixd 2024-03-03 18:10:30 +03:00
7f5553316c merged 2024-03-03 17:40:11 +03:00
Tony
b43ba41f9e
Merge branch 'dev' into hotfix/posting-author 2024-03-03 17:36:14 +03:00
Tony
312f0f5cc9
Merge pull request #422 from Discours/hotfix/all-authors-ssr
Hotfix/all authors ssr
2024-03-03 17:27:33 +03:00
6bee204280 myrate-update-fix 2024-03-03 17:26:39 +03:00
bbb5ad435a merged 2024-03-03 17:19:17 +03:00
Tony
9a7c973bb2
Merge branch 'dev' into hotfix/all-authors-ssr 2024-03-02 10:32:59 +03:00
780f59f517 feed-comments-order-fix 2024-03-01 16:04:28 +03:00
d0be8ffb6a minor 2024-03-01 00:11:59 +03:00
72610d10b5 wip 2024-02-29 23:54:34 +03:00
423af46377 fixed 2024-02-29 23:46:15 +03:00
deebe79f55 comments-hotfix 2024-02-29 20:51:07 +03:00
a87efec9dc Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-02-29 18:10:56 +03:00
abc2d01485 comments-order-hotfix 2024-02-29 18:05:05 +03:00
ilya-bkv
72cb6325d9 Fix comments date in Feed page 2024-02-29 17:33:17 +03:00
ilya-bkv
c9d79088d8 Fix link border bottom color 2024-02-29 17:24:34 +03:00
9e513b2430 drafts-order-fix 2024-02-29 16:08:35 +03:00
1a3d7a9520 Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev 2024-02-29 09:53:00 +03:00
147417d172 gql-hotfix 2024-02-29 09:51:39 +03:00
Ilya Y
b1b3f8b435
auth modals fix logic (#420)
auth modals fix logic
2024-02-27 17:42:54 +03:00
Ilya Y
8b066104e6
Hotfix/can edit fix (#421)
Fix canEdit() in Commetns
2024-02-27 14:41:49 +03:00
ilya-bkv
0d1aaebfa5 clearSearchParams fix 2024-02-26 15:24:03 +03:00
ilya-bkv
1d8ffe64b5 Div 2024-02-26 15:18:56 +03:00
ilya-bkv
6e1e46fca2 Cleanup code 2024-02-26 14:56:40 +03:00
ilya-bkv
80e338a60d Cleanup code 2024-02-26 14:38:48 +03:00
ilya-bkv
e34aa5e70b fix authors list pages 2024-02-26 14:38:23 +03:00
ilya-bkv
507cfebd48 Cleanup code 2024-02-26 14:19:12 +03:00
55ebc1c634 space-fix 2024-02-26 01:43:15 +03:00
bfc7ed15d8 sort-order-fix 2024-02-26 01:40:09 +03:00
96c52ae2b4 load-author-hotfix 2024-02-26 01:29:26 +03:00
132418d539 version-sync 2024-02-26 00:50:26 +03:00
5092a98fba merged 2024-02-26 00:44:58 +03:00
f5295d2c3d stat-api-hotfix 2024-02-26 00:20:55 +03:00
Ilya Y
73e1f575f8
Hotfix/all authors bugfix (#418)
bufgix to authors
2024-02-25 10:31:11 +03:00
Ilya Y
fe9fd37d9d
Fix getRandomTopics (#419) 2024-02-25 10:04:05 +03:00
c2035b801a update-fixxd 2024-02-17 21:57:02 +03:00
3f7679710f minor 2024-02-17 18:44:56 +03:00
560739627a more-defined 2024-02-17 18:13:54 +03:00
0dd2736dd5 catch-response-on-update 2024-02-17 18:03:01 +03:00
3a6faa65a8 tolerate-fails-more 2024-02-17 17:40:10 +03:00
e32e3d31ea fmt 2024-02-17 17:31:08 +03:00
002ffe64fc parse-tolerate 2024-02-17 17:28:57 +03:00
6fa6076f9f editor-context-fixes 2024-02-17 17:22:11 +03:00
20e4e985f5 fmt 2024-02-17 16:25:25 +03:00
748bd206d1 small-fixes 2024-02-17 16:03:47 +03:00
b34da48e8c Merge remote-tracking branch 'hub/hotfix/editor-adapter' into feature/rating 2024-02-16 21:51:17 +03:00
a6a825d623 pkg-up 2024-02-16 21:51:05 +03:00
855953d888 Merge branch 'feature/editor-buttons' into feature/rating 2024-02-16 21:49:39 +03:00
bfdaeb475b schema-redeploy 2024-02-16 21:47:42 +03:00
1deff46de8 gql-fix 2024-02-16 20:07:00 +03:00
e2a829f44f modal-link-fix
All checks were successful
deploy / test (push) Successful in 2m3s
deploy / Update templates on Mailgun (push) Has been skipped
2024-02-16 14:30:29 +03:00
6451118e90 bump-with-link 2024-02-16 14:29:27 +03:00
e5ce4585fd not-a-vite-but 2024-02-16 14:11:17 +03:00
cb2781bad8 hide-voters-for-reader 2024-02-16 13:59:32 +03:00
4d0291bd9f vite-rollback
All checks were successful
deploy / test (push) Successful in 2m42s
deploy / Update templates on Mailgun (push) Has been skipped
2024-02-16 12:00:23 +03:00
b0b7cf424d reactivity+ 2024-02-16 11:29:06 +03:00
dfb2b17116 rating-reactivity-fixing-2 2024-02-16 11:02:00 +03:00
dab1eff314 rating-reactivity-fixing 2024-02-16 11:01:40 +03:00
54ef10307e dont-hesitate 2024-02-15 22:47:02 +03:00
94c5e5d51d some-space 2024-02-15 22:28:07 +03:00
1655fb1c25 ignore-py 2024-02-15 22:19:39 +03:00
f7f14328cf Merge remote-tracking branch 'hub/dev' into feature/rating 2024-02-15 22:08:05 +03:00
189f0beace trig-deploy 2024-02-15 21:14:29 +03:00
0eeef56369 pkgs-upgrade 2024-02-15 19:55:23 +03:00
507a685019 no-lintstaged 2024-02-15 17:26:27 +03:00
8957167dfa vite-config-fix 2024-02-15 16:59:15 +03:00
21cf2b6185 packages-upgrade 2024-02-15 16:57:09 +03:00
7ccf3f8ce1 Merge remote-tracking branch 'hub/dev' into feature/rating 2024-02-15 16:49:40 +03:00
09a907d123 reactive-fix 2024-02-15 16:41:14 +03:00
e336754226 refactoring-ratings 2024-02-15 15:51:04 +03:00
82c6841523 edge-fix 2024-02-15 14:53:57 +03:00
1aef9cc952 postmerge 2024-02-15 14:46:11 +03:00
90a70b4097 lintpass 2024-02-08 20:03:04 +03:00
09e6ab009f bunfix 2024-02-08 17:01:04 +03:00
d4c47f3ec7 postmerge 2024-02-08 04:14:17 +03:00
4bc3a27254 debug-wip
All checks were successful
deploy / test (push) Successful in 2m13s
deploy / Update templates on Mailgun (push) Has been skipped
2024-02-07 19:54:52 +03:00
604 changed files with 35443 additions and 18773 deletions

View File

@ -5,7 +5,6 @@ on: [push]
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref != 'refs/heads/feature/email-templates'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
@ -19,10 +18,10 @@ jobs:
run: npm install --global --save-exact @biomejs/biome run: npm install --global --save-exact @biomejs/biome
- name: Lint with Biome - name: Lint with Biome
run: npx biome ci . run: npx @biomejs/biome ci
- name: Lint styles - name: Lint styles
run: npm run lint:styles run: npx stylelint **/*.{scss,css}
- name: Check types - name: Check types
run: npm run typecheck run: npm run typecheck
@ -30,48 +29,52 @@ jobs:
- name: Test production build - name: Test production build
run: npm run build run: npm run build
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run e2e
env:
BASE_URL: ${{ github.event.deployment_status.target_url }}
DEBUG: pw:api
email-templates: email-templates:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Update templates on Mailgun name: Update templates on Mailgun
if: github.event_name == 'push' && github.ref == 'refs/heads/feature/email-templates' if: github.event_name == 'push' && github.ref == 'refs/heads/feature/email-templates'
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v3 - name: Checkout
- uses: actions/setup-node@v3 uses: actions/checkout@v2
with:
node-version: '18'
- name: Run templates build - name: "Email confirmation template"
run: npm run templates
- name: "authorizer_email_confirmation template"
uses: gyto/mailgun-template-action@v2 uses: gyto/mailgun-template-action@v2
with: with:
html-file: "./templates/dist/authorizer_email_confirmation.html" html-file: "./templates/authorizer_email_confirmation.html"
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }} mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
mailgun-domain: "discours.io" mailgun-domain: "discours.io"
mailgun-template: "authorizer_email_confirmation" mailgun-template: "authorizer_email_confirmation"
- name: "authorizer_password_reset template" - name: "Password reset template"
uses: gyto/mailgun-template-action@v2 uses: gyto/mailgun-template-action@v2
with: with:
html-file: "./templates/dist/authorizer_password_reset.html" html-file: "./templates/authorizer_password_reset.html"
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }} mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
mailgun-domain: "discours.io" mailgun-domain: "discours.io"
mailgun-template: "authorizer_password_reset" mailgun-template: "authorizer_password_reset"
- name: "email_first_publication template deploy" - name: "First publication notification"
uses: gyto/mailgun-template-action@v2 uses: gyto/mailgun-template-action@v2
with: with:
html-file: "./templates/dist/authorizer_first_publication.html" html-file: "./templates/first_publication_notification.html"
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }} mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
mailgun-domain: "discours.io" mailgun-domain: "discours.io"
mailgun-template: "email_first_publication" mailgun-template: "first_publication_notification"
- name: "new_comment_notification template" - name: "New comment notification template"
uses: gyto/mailgun-template-action@v2 uses: gyto/mailgun-template-action@v2
with: with:
html-file: "./templates/dist/authorizer_new_comment.html" html-file: "./templates/new_comment_notification.html"
mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }} mailgun-api-key: ${{ secrets.MAILGUN_API_KEY }}
mailgun-domain: "discours.io" mailgun-domain: "discours.io"
mailgun-template: "new_comment_notification" mailgun-template: "new_comment_notification"

View File

@ -1,42 +1,58 @@
name: "deploy" name: "CI and E2E Tests"
on: [push] on:
push:
deployment_status:
types: [success]
jobs: jobs:
test: ci:
if: github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
- name: Install dependencies - name: Install dependencies
run: npm i
- name: Install CI checks
run: npm ci run: npm ci
- name: Check types - name: Check types
run: npm run typecheck run: npm run typecheck
- name: Lint with Biome - name: Lint with Biome
run: npx biome ci . run: npx @biomejs/biome check src/.
- name: Lint styles - name: Lint styles
run: npm run lint:styles run: npx stylelint **/*.{scss,css}
- name: Test production build - name: Test production build
run: npm run build run: npm run build
e2e: e2e_tests:
timeout-minutes: 60 needs: ci
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps: steps:
- name: Debug event info
run: |
echo "Event Name: ${{ github.event_name }}"
echo "Deployment Status: ${{ github.event.deployment_status.state }}"
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm install
- name: Install Playwright - name: Wait for deployment to be live
run: npx playwright install --with-deps run: |
echo "Waiting for Vercel deployment to be live..."
until curl -sSf https://testing.discours.io > /dev/null; do
printf '.'
sleep 10
done
- name: Install Playwright and dependencies
run: npm run e2e:install
- name: Run Playwright tests - name: Run Playwright tests
run: npx playwright test run: npm run e2e:tests:ci
env: env:
BASE_URL: ${{ github.event.deployment_status.target_url }} BASE_URL: https://testing.discours.io
continue-on-error: true
- name: Report test result if failed
if: failure()
run: echo "E2E tests failed. Please review the logs."

12
.gitignore vendored
View File

@ -1,8 +1,9 @@
.devcontainer
.pnpm-store
dist/ dist/
node_modules/ node_modules/
npm-debug.log* npm-debug.log*
pnpm-debug.log* pnpm-debug.log*
.vscode
.env .env
.env.production .env.production
.DS_Store .DS_Store
@ -22,4 +23,11 @@ bun.lockb
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/plawright-report/ /plawright-report/
/templates/dist/* target
.github/dependabot.yml
.output
.vinxi
*.pem
edge.*
.vscode/settings.json
storybook-static

View File

@ -1,5 +0,0 @@
{
"*.{js,ts,cjs,mjs,d.mts,jsx,tsx,json,jsonc}": [
"npx @biomejs/biome check ./src && tsc"
]
}

49
.storybook/main.ts Normal file
View File

@ -0,0 +1,49 @@
import type { FrameworkOptions, StorybookConfig } from 'storybook-solidjs-vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-themes',
'storybook-addon-sass-postcss'
],
framework: {
name: 'storybook-solidjs-vite',
options: {
builder: {
viteConfigPath: './vite.config.ts'
}
} as FrameworkOptions
},
docs: {
autodocs: 'tag'
},
viteFinal: (config) => {
if (config.build) {
config.build.sourcemap = true
config.build.minify = process.env.NODE_ENV === 'production'
}
if (config.css) {
config.css.preprocessorOptions = {
scss: {
silenceDeprecations: ['mixed-decls'],
additionalData: '@import "~/styles/imports";\n',
includePaths: ['./public', './src/styles', './node_modules']
}
}
}
return config
},
previewHead: (head) => `
${head}
<style>
body {
transition: none !important;
}
</style>
`
}
export default config

34
.storybook/preview.ts Normal file
View File

@ -0,0 +1,34 @@
import { withThemeByClassName } from '@storybook/addon-themes'
import '../src/styles/app.scss'
const preview = {
parameters: {
themes: {
default: 'light',
list: [
{ name: 'light', class: '', color: '#f8fafc' },
{ name: 'dark', class: 'dark', color: '#0f172a' }
]
},
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/
}
}
}
}
export default preview
export const decorators = [
withThemeByClassName({
themes: {
light: '',
dark: 'dark'
},
defaultTheme: 'light',
parentSelector: 'body'
})
]

23
.storybook/test-runner.ts Normal file
View File

@ -0,0 +1,23 @@
import type { Page } from '@playwright/test'
import type { TestRunnerConfig } from '@storybook/test-runner'
import { checkA11y, injectAxe } from 'axe-playwright'
/*
* See https://storybook.js.org/docs/react/writing-tests/test-runner#test-hook-api-experimental
* to learn more about the test-runner hooks API.
*/
const a11yConfig = {
async preRender(page: Page) {
await injectAxe(page)
},
async postRender(page: Page) {
await checkA11y(page, '#storybook-root', {
detailedReport: true,
detailedReportOptions: {
html: true
}
})
}
} as TestRunnerConfig
module.exports = a11yConfig

View File

@ -1,2 +1,6 @@
.vercel/ node_modules
dist/ dist/
storybook-static
.output
.vinxi
.vercel

View File

@ -1,34 +1,73 @@
{ {
"extends": ["stylelint-config-standard-scss"], "defaultSeverity": "warning",
"extends": ["stylelint-config-standard-scss", "stylelint-config-recommended"],
"plugins": ["stylelint-order", "stylelint-scss"], "plugins": ["stylelint-order", "stylelint-scss"],
"rules": { "rules": {
"keyframes-name-pattern": null, "annotation-no-unknown": [
"declaration-block-no-redundant-longhand-properties": null,
"selector-class-pattern": null,
"no-descending-specificity": null,
"scss/function-no-unknown": null,
"scss/no-global-function-names": null,
"function-url-quotes": null,
"font-family-no-missing-generic-family-keyword": null,
"order/order": ["custom-properties", "declarations"],
"scss/dollar-variable-pattern": [
"^[a-z][a-zA-Z]+$",
{
"ignore": "global"
}
],
"selector-pseudo-class-no-unknown": [
true, true,
{ {
"ignorePseudoClasses": ["global", "export"] "ignoreAnnotations": ["default"]
} }
], ],
"at-rule-no-unknown": null,
"declaration-block-no-redundant-longhand-properties": null,
"font-family-no-missing-generic-family-keyword": null,
"function-no-unknown": [
true,
{
"ignoreFunctions": ["divide", "transparentize"]
}
],
"function-url-quotes": null,
"keyframes-name-pattern": null,
"no-descending-specificity": null,
"order/order": [
{
"type": "at-rule",
"name": "include"
},
"custom-properties",
"declarations",
"rules"
],
"property-no-vendor-prefix": [ "property-no-vendor-prefix": [
true, true,
{ {
"ignoreProperties": ["box-decoration-break"] "ignoreProperties": ["box-decoration-break"]
} }
],
"scss/at-function-pattern": null,
"scss/at-mixin-pattern": null,
"scss/dollar-variable-colon-space-after": "always-single-line",
"scss/dollar-variable-colon-space-before": "never",
"scss/dollar-variable-pattern": [
"^[a-z][a-zA-Z]+$",
{
"ignore": "global"
}
],
"scss/double-slash-comment-empty-line-before": [
"always",
{
"except": ["first-nested"],
"ignore": ["between-comments", "stylelint-commands"]
}
],
"scss/double-slash-comment-whitespace-inside": "always",
"scss/function-no-unknown": null,
"scss/no-duplicate-dollar-variables": null,
"scss/no-duplicate-mixins": null,
"scss/no-global-function-names": null,
"scss/operator-no-newline-after": null,
"scss/operator-no-newline-before": null,
"scss/operator-no-unspaced": null,
"scss/percent-placeholder-pattern": null,
"selector-class-pattern": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "export"]
}
] ]
}, }
"defaultSeverity": "warning"
} }

3
.vscode/extension.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome", "stylelint.vscode-stylelint", "wayou.vscode-todo-highlight"]
}

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "always"
}
}

57
README.en.md Normal file
View File

@ -0,0 +1,57 @@
## Development setup recommendations
### How to start
Use `bun i`, `npm i`, `pnpm i` or `yarn` to install packages.
### Config of variables
- Use `.env` file to setup your own development environment
- Env vars with prefix `PUBLIC_` are widely used in `/src/utils/config.ts`
### Useful commands
run checks, fix styles, imports, formatting and autofixable linting errors:
```
bun run typecheck
bun run fix
```
## End-to-End (E2E) Tests
This directory contains end-to-end tests. These tests are written using [Playwright](https://playwright.dev/)
### Structure
- `/tests/*`: This directory contains the test files.
- `/playwright.config.ts`: This is the configuration file for Playwright.
### Getting Started
Follow these steps:
1. **Install dependencies**: Run `npm run e2e:install` to install the necessary dependencies for running the tests.
2. **Run the tests**: After using `npm run e2e:tests`.
### Additional Information
If workers is no needed use:
- `npx playwright test --project=webkit --workers 4`
For more information on how to write tests using Playwright - [Playwright documentation](https://playwright.dev/docs/intro).
### 🚀 Tests in CI Mode
Tests are executed within a GitHub workflow. We organize our tests into two main directories:
- `tests`: Contains tests that do not require authentication.
- `tests-with-auth`: Houses tests that interact with authenticated parts of the application.
🔧 **Configuration:**
Playwright is configured to utilize the `BASE_URL` environment variable. Ensure this is properly set in your CI configuration to point to the correct environment.
📝 **Note:**
After pages have been adjusted to work with authentication, all tests should be moved to the `tests` directory to streamline the testing process.

View File

@ -1,30 +1,57 @@
## How to start [English](README.en.md)
## Рекомендации по настройке разработки
### Как начать
Используйте `bun i`, `npm i`, `pnpm i` или `yarn`, чтобы установить пакеты.
### Настройка переменных
- Используйте файл `.env` для настройки переменных собственной среды разработки.
- Переменные окружения с префиксом `PUBLIC_` широко используются в `/src/utils/config.ts`.
### Полезные команды
Запуск проверки соответствия типов и автоматически исправить ошибки стилей, порядок импорта, форматирование:
``` ```
npm install bun run typecheck
npm start bun run fix
``` ```
## Useful commands ## End-to-End (E2E) тесты
run checks
```
npm run check
```
type checking with watch
```
npm run typecheck:watch
```
fix styles, imports, formatting and autofixable linting errors:
```
npm run fix
```
## Code generation
generate new SolidJS component: End-to-end тесты написаны с использованием [Playwright](https://playwright.dev/).
```
npm run hygen component new NewComponentName
```
generate new SolidJS context: ### Структура
```
npm run hygen context new NewContextName - `/tests/*`: содержит файлы тестов
``` - `/playwright.config.ts`: конфиг для Playwright
### Начало работы
Следуйте этим шагам:
1. **Установите зависимости**: Запустите `npm run e2e:install`, чтобы установить необходимые зависимости для выполнения тестов.
2. **Запустите тесты**: После установки зависимостей используйте `npm run e2e:tests`.
### Дополнительная информация
Для параллельного исполнения:
- `npx playwright test --project=webkit --workers 4`
Для получения дополнительной информации о написании тестов с использованием Playwright - [Документация Playwright](https://playwright.dev/docs/intro).
### 🚀 Тесты в режиме CI
Тесты выполняются в рамках GitHub workflow из папки `tests`
🔧 **Конфигурация:**
Playwright настроен на использование переменной окружения `BASE_URL`. Убедитесь, что она правильно установлена в вашей конфигурации CI для указания на правильную среду.
📝 **Примечание:**
После того как страницы были настроены для работы с аутентификацией, все тесты должны быть перемещены в директорию `tests` для упрощения процесса тестирования.

View File

@ -1,32 +0,0 @@
import { renderPage } from 'vike/server'
export const config = {
runtime: 'edge'
}
export default async function handler(request) {
const { url, cookies } = request
const pageContext = await renderPage({ urlOriginal: url, cookies })
const { httpResponse, errorWhileRendering, is404 } = pageContext
if (errorWhileRendering && !is404) {
console.error(errorWhileRendering)
return new Response('', { status: 500 })
}
if (!httpResponse) {
return new Response()
}
const { body, statusCode, headers: headersArray } = httpResponse
const headers = headersArray.reduce((acc, [name, value]) => {
acc[name] = value
return acc
}, {})
headers['Cache-Control'] = 's-maxage=1, stale-while-revalidate'
return new Response(body, { status: statusCode, headers })
}

View File

@ -1,10 +1,8 @@
const formData = require('form-data') import FormData from 'form-data'
const Mailgun = require('mailgun.js') import Mailgun from 'mailgun.js'
const mailgun = new Mailgun(formData) const mailgun = new Mailgun(FormData)
const mg = mailgun.client({ username: 'discoursio', key: process.env.MAILGUN_API_KEY })
const { MAILGUN_API_KEY, MAILGUN_DOMAIN } = process.env
const mg = mailgun.client({ username: 'discoursio', key: MAILGUN_API_KEY })
export default async function handler(req, res) { export default async function handler(req, res) {
const { contact, subject, message } = req.body const { contact, subject, message } = req.body
@ -19,7 +17,7 @@ export default async function handler(req, res) {
} }
try { try {
const response = await mg.messages.create(MAILGUN_DOMAIN, data) const response = await mg.messages.create('discours.io', data)
console.log('Email sent successfully!', response) console.log('Email sent successfully!', response)
res.status(200).json({ result: 'great success' }) res.status(200).json({ result: 'great success' })
} catch (error) { } catch (error) {

View File

@ -1,10 +1,8 @@
const formData = require('form-data') import FormData from 'form-data'
const Mailgun = require('mailgun.js') import Mailgun from 'mailgun.js'
const mailgun = new Mailgun(formData) const mailgun = new Mailgun(FormData)
const mg = mailgun.client({ username: 'discoursio', key: process.env.MAILGUN_API_KEY })
const { MAILGUN_API_KEY } = process.env
const mg = mailgun.client({ username: 'discoursio', key: MAILGUN_API_KEY })
export default async (req, res) => { export default async (req, res) => {
const { email } = req.body const { email } = req.body

23
app.config.ts Normal file
View File

@ -0,0 +1,23 @@
import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config'
import viteConfig, { isDev } from './vite.config'
const isVercel = Boolean(process.env.VERCEL)
const isNetlify = Boolean(process.env.NETLIFY)
const isBun = Boolean(process.env.BUN)
const preset = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node'
console.info(`[app.config] solid-start preset {> ${preset} <}`)
export default defineConfig({
nitro: {
timing: true
},
ssr: true,
server: {
preset,
port: 3000,
https: true
},
devOverlay: isDev,
vite: viteConfig
} as SolidStartInlineConfig)

View File

@ -1,16 +1,18 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
"files": { "files": {
"include": ["*.tsx", "*.ts", "*.js", "*.json"], "include": ["*.tsx", "*.ts", "*.js", "*.json"],
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "templates"] "ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
}, },
"vcs": { "vcs": {
"defaultBranch": "dev", "defaultBranch": "dev",
"useIgnoreFile": true "useIgnoreFile": true,
"enabled": true,
"clientKind": "git"
}, },
"organizeImports": { "organizeImports": {
"enabled": true, "enabled": true,
"ignore": ["./api", "./gen"] "ignore": ["./gen"]
}, },
"formatter": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
@ -22,10 +24,10 @@
"formatter": { "formatter": {
"semicolons": "asNeeded", "semicolons": "asNeeded",
"quoteStyle": "single", "quoteStyle": "single",
"trailingComma": "none",
"enabled": true, "enabled": true,
"jsxQuoteStyle": "double", "jsxQuoteStyle": "double",
"arrowParentheses": "always" "arrowParentheses": "always",
"trailingCommas": "none"
} }
}, },
"linter": { "linter": {
@ -36,10 +38,13 @@
"complexity": { "complexity": {
"noForEach": "off", "noForEach": "off",
"useOptionalChain": "warn", "useOptionalChain": "warn",
"useLiteralKeys": "off" "useLiteralKeys": "off",
"noExcessiveCognitiveComplexity": "off"
}, },
"correctness": { "correctness": {
"useHookAtTopLevel": "off" "useHookAtTopLevel": "off",
"useImportExtensions": "off",
"noUndeclaredDependencies": "off"
}, },
"a11y": { "a11y": {
"useHeadingContent": "off", "useHeadingContent": "off",
@ -51,20 +56,28 @@
"useAltText": "off", "useAltText": "off",
"useButtonType": "off", "useButtonType": "off",
"noRedundantAlt": "off", "noRedundantAlt": "off",
"noSvgWithoutTitle": "off" "noSvgWithoutTitle": "off",
"noLabelWithoutControl": "off"
}, },
"nursery": { "nursery": {
"useImportRestrictions": "off", "useImportRestrictions": "off"
"useImportType": "off", },
"useFilenamingConvention": "off" "performance": {
"noBarrelFile": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off",
"noNamespaceImport": "warn",
"useBlockStatements": "off", "useBlockStatements": "off",
"noImplicitBoolean": "off", "noImplicitBoolean": "off",
"useNamingConvention": "off", "useNamingConvention": "off",
"noDefaultExport": "off" "useImportType": "off",
"noDefaultExport": "off",
"useFilenamingConvention": "off",
"useExplicitLengthCheck": "off"
}, },
"suspicious": { "suspicious": {
"noConsole": "off",
"noConsoleLog": "off", "noConsoleLog": "off",
"noAssignInExpressions": "off" "noAssignInExpressions": "off"
} }

View File

@ -25,33 +25,3 @@ generates:
useTypeImports: true useTypeImports: true
outputPath: './src/graphql/types/core.gen.ts' outputPath: './src/graphql/types/core.gen.ts'
# namingConvention: change-case#CamelCase # for generated types # namingConvention: change-case#CamelCase # for generated types
# Generate types for notifier
src/graphql/schema/notifier.gen.ts:
schema: 'https://notifier.discours.io'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-urql'
config:
skipTypename: true
useTypeImports: true
outputPath: './src/graphql/types/notifier.gen.ts'
# namingConvention: change-case#CamelCase # for generated types
# internal types for auth
# src/graphql/schema/auth.gen.ts:
# schema: 'https://auth.discours.io/graphql'
# plugins:
# - 'typescript'
# - 'typescript-operations'
# - 'typescript-urql'
# config:
# skipTypename: true
# useTypeImports: true
# outputPath: './src/graphql/types/auth.gen.ts'
# namingConvention: change-case#CamelCase # for generated types
hooks:
afterAllFileWrite:
- prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/schema/*.gen.ts

View File

@ -1,66 +0,0 @@
@startuml
actor User
participant Browser
participant Vercel
participant article.page.server.ts
participant Solid
participant Store
User -> Browser: discours.io
activate Browser
Browser -> Vercel: GET <slug>
activate Vercel
Vercel -> article.page.server.ts: render
activate article.page.server.ts
article.page.server.ts -> apiClient: getArticle({ slug })
activate apiClient
apiClient -> DB: query: articleBySlug
activate DB
DB --> apiClient: response
deactivate DB
apiClient --> article.page.server.ts: article data
deactivate apiClient
article.page.server.ts -> Solid: render <ArticlePage article={article} />
activate Solid
Solid -> Store: useCurrentArticleStore(article)
activate Store
Store -> Store: create store with initial data (server)
Store --> Solid: currentArticle
deactivate Store
Solid -> Solid: render component
Solid --> article.page.server.ts: rendered component
deactivate Solid
article.page.server.ts --> Vercel: rendered page
Vercel -> Vercel: save rendered page to CDN
deactivate article.page.server.ts
Vercel --> Browser: rendered page
deactivate Vercel
Browser --> User: rendered page
deactivate Browser
Browser -> Browser: load client scripts
Browser -> Solid: render <ArticlePage article={article} />
Solid -> Store: useCurrentArticleStore(article)
activate Store
Store -> Store: create store with initial data (client)
Store --> Solid: currentArticle
deactivate Store
Solid -> Solid: render component (no changes)
Solid -> Solid: onMount
Solid -> Store: loadArticleComments
activate Store
Store -> apiClient: getArticleComments
activate apiClient
apiClient -> DB: query: getReactions
activate DB
DB --> apiClient: response
deactivate DB
apiClient --> Store: comments data
deactivate apiClient
Store -> Store: update store
Store --> Solid: store updated
deactivate Store
Solid -> Solid: render comments
Solid --> Browser: rendered comments
Browser --> User: comments
@enduml

View File

@ -1,40 +0,0 @@
@startuml
actor User
participant Browser
participant Server
User -> Browser: discours.io
activate Browser
Browser -> Server: GET\nquery { lng }\ncookies { lng }
opt lng in query
Server -> Server: lng = lng from query
else no lng in query
opt lng in cookies
Server -> Server: lng = lng from cookies
else no lng in cookies
Server -> Server: lng = 'ru'
end opt
end opt
note right
_dafault.page.server.ts render
end note
opt i18next is not initialized
Server -> Server: initialize i18next with lng
else i18next not initialized
Server -> Server: change i18next language to lng
end opt
note right
all resources loaded synchronously
end note
Server --> Browser: pageContext { lng }
Browser -> Browser: init client side i18next with http backend
activate Browser
Browser -> Server: get translations for current language
Server --> Browser: translations JSON
deactivate Browser
Browser -> Browser: render page
Browser --> User: rendered page
deactivate Browser
@enduml

View File

@ -1,24 +0,0 @@
@startuml
actor User
participant Browser
participant Server
User -> Browser: discours.io
activate Browser
Browser -> Server: GET
activate Server
Server -> Server: resolve route
note right
based on routes from
*.page.route.ts files
end note
Server -> Server: some.page.server.ts onBeforeRender
Server -> Server: _default.page.server.tsx render
Server --> Browser: pageContent
deactivate Server
Browser -> Browser: _default.page.client.tsx render(pageContext)
Browser --> User: rendered page
deactivate Browser
@enduml

View File

@ -1,18 +0,0 @@
---
to: src/components/<%= h.changeCase.pascal(name) %>/<%= h.changeCase.pascal(name) %>.tsx
---
import { clsx } from 'clsx'
import styles from './<%= h.changeCase.pascal(name) %>.module.scss'
type Props = {
class?: string
}
export const <%= h.changeCase.pascal(name) %> = (props: Props) => {
return (
<div class={clsx(styles.<%= h.changeCase.pascal(name) %>, props.class)}>
<%= h.changeCase.pascal(name) %>
</div>
)
}

View File

@ -1,4 +0,0 @@
---
to: src/components/<%= h.changeCase.pascal(name) %>/index.ts
---
export { <%= h.changeCase.pascal(name) %> } from './<%= h.changeCase.pascal(name) %>'

View File

@ -1,7 +0,0 @@
---
to: src/components/<%= h.changeCase.pascal(name) %>/<%= h.changeCase.pascal(name) %>.module.scss
---
.<%= h.changeCase.pascal(name) %> {
display: block;
}

View File

@ -1,24 +0,0 @@
---
to: src/context/<%= h.changeCase.camel(name) %>.tsx
---
import type { Accessor, JSX } from 'solid-js'
import { createContext, createSignal, useContext } from 'solid-js'
type <%= h.changeCase.pascal(name) %>ContextType = {
}
const <%= h.changeCase.pascal(name) %>Context = createContext<<%= h.changeCase.pascal(name) %>ContextType>()
export function use<%= h.changeCase.pascal(name) %>() {
return useContext(<%= h.changeCase.pascal(name) %>Context)
}
export const <%= h.changeCase.pascal(name) %>Provider = (props: { children: JSX.Element }) => {
const actions = {
}
const value: <%= h.changeCase.pascal(name) %>ContextType = { ...actions }
return <<%= h.changeCase.pascal(name) %>Context.Provider value={value}>{props.children}</<%= h.changeCase.pascal(name) %>Context.Provider>
}

View File

@ -1,5 +0,0 @@
---
message: |
hygen {bold generator new} --name [NAME] --action [ACTION]
hygen {bold generator with-prompt} --name [NAME] --action [ACTION]
---

View File

@ -1,16 +0,0 @@
---
to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
---
---
to: app/hello.js
---
const hello = ```
Hello!
This is your first hygen template.
Learn what it can do here:
https://github.com/jondot/hygen
```
console.log(hello)

View File

@ -1,16 +0,0 @@
---
to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
---
---
to: app/hello.js
---
const hello = ```
Hello!
This is your first prompt based hygen template.
Learn what it can do here:
https://github.com/jondot/hygen
```
console.log(hello)

View File

@ -1,14 +0,0 @@
---
to: gen/<%= name %>/<%= action || 'new' %>/prompt.js
---
// see types of prompts:
// https://github.com/enquirer/enquirer/tree/master/examples
//
module.exports = [
{
type: 'input',
name: 'message',
message: "What's your message?"
}
]

View File

@ -1,4 +0,0 @@
---
setup: <%= name %>
force: true # this is because mostly, people init into existing folders is safe
---

21683
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,143 +1,151 @@
{ {
"name": "discoursio-webapp", "name": "discoursio-webapp",
"version": "0.9.2",
"private": true, "private": true,
"license": "MIT", "version": "0.9.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "vite build", "dev": "vinxi dev",
"check": "npm run lint && npm run typecheck", "build": "vinxi build",
"start": "vinxi start",
"codegen": "graphql-codegen", "codegen": "graphql-codegen",
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel", "e2e": "E2E=1 npm run e2e:tests",
"dev": "vite", "e2e:tests": "npx playwright test --project=webkit",
"e2e": "npx playwright test --project=chromium", "e2e:tests:ci": "CI=true npx playwright test --project=webkit",
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix", "e2e:install": "npx playwright install webkit && npx playwright install-deps ",
"format": "npx @biomejs/biome format . --write", "fix": "npx @biomejs/biome check . --fix && stylelint **/*.{scss,css} --fix",
"hygen": "HYGEN_TMPLS=gen hygen", "format": "npx @biomejs/biome format src/. --write",
"postinstall": "npm run codegen && npx patch-package", "postinstall": "npm run codegen && npx patch-package",
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
"check:code:fix": "npx @biomejs/biome check src --log-kind=compact --verbose --apply-unsafe",
"lint": "npm run lint:code && stylelint **/*.{scss,css}",
"lint:code": "npx @biomejs/biome lint . --log-kind=compact --verbose",
"lint:code:fix": "npx @biomejs/biome lint . --apply-unsafe --log-kind=compact --verbose",
"lint:styles": "stylelint **/*.{scss,css}",
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
"preview": "vite preview",
"start": "vite",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch", "storybook": "storybook dev -p 6006",
"templates": "node ./templates/compile.cjs" "storybook:test": "test-storybook",
}, "build-storybook": "storybook build"
"dependencies": {
"form-data": "4.0.0",
"mailgun.js": "10.1.0"
}, },
"devDependencies": { "devDependencies": {
"@authorizerdev/authorizer-js": "2.0.0", "@authorizerdev/authorizer-js": "^2.0.3",
"@babel/core": "7.23.3", "@biomejs/biome": "^1.9.3",
"@biomejs/biome": "^1.5.3", "@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/typescript": "^4.0.9",
"@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript-operations": "^4.2.3",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql": "^4.0.0", "@graphql-codegen/typescript-urql": "^4.0.0",
"@graphql-tools/url-loader": "8.0.1", "@hocuspocus/provider": "^2.13.6",
"@hocuspocus/provider": "2.11.0", "@playwright/test": "^1.47.2",
"@microsoft/fetch-event-source": "^2.0.1", "@popperjs/core": "^2.11.8",
"@nanostores/router": "0.13.0", "@solid-primitives/media": "^2.2.9",
"@nanostores/solid": "0.4.2", "@solid-primitives/memo": "^1.3.9",
"@playwright/test": "1.41.2", "@solid-primitives/pagination": "^0.3.0",
"@popperjs/core": "2.11.8", "@solid-primitives/script-loader": "^2.2.0",
"@sentry/browser": "7.99.0", "@solid-primitives/share": "^2.0.6",
"@solid-primitives/media": "2.2.3", "@solid-primitives/storage": "^4.2.1",
"@solid-primitives/memo": "1.2.4", "@solid-primitives/upload": "^0.0.117",
"@solid-primitives/pagination": "0.2.10", "@solidjs/meta": "^0.29.4",
"@solid-primitives/share": "2.0.4", "@solidjs/router": "^0.14.7",
"@solid-primitives/storage": "1.3.9", "@solidjs/start": "^1.0.8",
"@solid-primitives/upload": "0.0.110", "@storybook/addon-a11y": "^8.3.4",
"@solidjs/meta": "0.29.1", "@storybook/addon-actions": "^8.3.4",
"@thisbeyond/solid-select": "0.14.0", "@storybook/addon-controls": "^8.3.4",
"@tiptap/core": "2.2.3", "@storybook/addon-essentials": "^8.3.4",
"@tiptap/extension-blockquote": "2.2.3", "@storybook/addon-interactions": "^8.3.4",
"@tiptap/extension-bold": "2.2.3", "@storybook/addon-links": "^8.3.4",
"@tiptap/extension-bubble-menu": "2.2.3", "@storybook/addon-themes": "^8.3.4",
"@tiptap/extension-bullet-list": "2.2.3", "@storybook/addon-viewport": "^8.3.4",
"@tiptap/extension-character-count": "2.2.3", "@storybook/builder-vite": "^8.3.4",
"@tiptap/extension-collaboration": "2.2.3", "@storybook/docs-tools": "^8.3.4",
"@tiptap/extension-collaboration-cursor": "2.2.3", "@storybook/test": "^8.3.4",
"@tiptap/extension-document": "2.2.3", "@storybook/test-runner": "^0.19.1",
"@tiptap/extension-dropcursor": "2.2.3", "@tiptap/core": "^2.8.0",
"@tiptap/extension-floating-menu": "2.2.3", "@tiptap/extension-blockquote": "^2.8.0",
"@tiptap/extension-focus": "2.2.3", "@tiptap/extension-bold": "^2.8.0",
"@tiptap/extension-gapcursor": "2.2.3", "@tiptap/extension-bubble-menu": "^2.8.0",
"@tiptap/extension-hard-break": "2.2.3", "@tiptap/extension-bullet-list": "^2.8.0",
"@tiptap/extension-heading": "2.2.3", "@tiptap/extension-character-count": "^2.8.0",
"@tiptap/extension-highlight": "2.2.3", "@tiptap/extension-collaboration": "^2.8.0",
"@tiptap/extension-history": "2.2.3", "@tiptap/extension-collaboration-cursor": "^2.8.0",
"@tiptap/extension-horizontal-rule": "2.2.3", "@tiptap/extension-document": "^2.8.0",
"@tiptap/extension-image": "2.2.3", "@tiptap/extension-dropcursor": "^2.8.0",
"@tiptap/extension-italic": "2.2.3", "@tiptap/extension-floating-menu": "^2.8.0",
"@tiptap/extension-link": "2.2.3", "@tiptap/extension-focus": "^2.8.0",
"@tiptap/extension-list-item": "2.2.3", "@tiptap/extension-gapcursor": "^2.8.0",
"@tiptap/extension-ordered-list": "2.2.3", "@tiptap/extension-hard-break": "^2.8.0",
"@tiptap/extension-paragraph": "2.2.3", "@tiptap/extension-heading": "^2.8.0",
"@tiptap/extension-placeholder": "2.2.3", "@tiptap/extension-highlight": "^2.8.0",
"@tiptap/extension-strike": "2.2.3", "@tiptap/extension-history": "^2.8.0",
"@tiptap/extension-text": "2.2.3", "@tiptap/extension-horizontal-rule": "^2.8.0",
"@tiptap/extension-underline": "2.2.3", "@tiptap/extension-image": "^2.8.0",
"@tiptap/extension-youtube": "2.2.3", "@tiptap/extension-italic": "^2.8.0",
"@types/js-cookie": "3.0.6", "@tiptap/extension-link": "^2.8.0",
"@types/node": "^20.11.0", "@tiptap/extension-list-item": "^2.8.0",
"@urql/core": "4.2.3", "@tiptap/extension-ordered-list": "^2.8.0",
"@urql/devtools": "^2.0.3", "@tiptap/extension-paragraph": "^2.8.0",
"babel-preset-solid": "1.8.4", "@tiptap/extension-placeholder": "^2.8.0",
"bootstrap": "5.3.2", "@tiptap/extension-strike": "^2.8.0",
"clsx": "2.0.0", "@tiptap/extension-text": "^2.8.0",
"cropperjs": "1.6.1", "@tiptap/extension-underline": "^2.8.0",
"cross-env": "7.0.3", "@tiptap/extension-youtube": "^2.8.0",
"fast-deep-equal": "3.1.3", "@tiptap/starter-kit": "^2.8.0",
"ga-gtag": "1.2.0", "@types/cookie": "^0.6.0",
"graphql": "16.8.1", "@types/cookie-signature": "^1.1.2",
"graphql-tag": "2.12.6", "@types/node": "^22.7.4",
"hygen": "6.2.11", "@types/throttle-debounce": "^5.0.2",
"i18next": "22.4.15", "@urql/core": "^5.0.6",
"i18next-http-backend": "2.2.0", "axe-playwright": "^2.0.3",
"i18next-icu": "2.3.0", "bootstrap": "^5.3.3",
"intl-messageformat": "10.5.3", "clsx": "^2.1.1",
"javascript-time-ago": "2.5.9", "cookie": "^0.6.0",
"js-cookie": "3.0.5", "cookie-signature": "^1.2.1",
"lint-staged": "15.1.0", "cropperjs": "^1.6.2",
"loglevel": "1.8.1", "extended-eventsource": "^1.6.4",
"loglevel-plugin-prefix": "0.8.4", "fast-deep-equal": "^3.1.3",
"nanostores": "0.9.5", "graphql": "^16.9.0",
"i18next": "^23.15.1",
"i18next-http-backend": "^2.6.1",
"i18next-icu": "^2.3.0",
"intl-messageformat": "^10.5.14",
"javascript-time-ago": "^2.5.11",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"prosemirror-history": "1.3.2", "prosemirror-history": "^1.4.1",
"prosemirror-trailing-node": "2.0.7", "prosemirror-trailing-node": "^2.0.9",
"prosemirror-view": "1.33.1", "prosemirror-view": "^1.34.3",
"rollup": "4.11.0", "rollup-plugin-visualizer": "^5.12.0",
"sass": "1.69.5", "sass": "1.77.6",
"solid-js": "1.8.15", "solid-js": "^1.9.1",
"solid-popper": "0.3.0", "solid-popper": "^0.3.0",
"solid-tiptap": "0.7.0", "solid-tiptap": "0.7.0",
"solid-transition-group": "0.2.3", "solid-transition-group": "^0.2.3",
"stylelint": "^16.0.0", "storybook": "^8.3.4",
"stylelint-config-standard-scss": "^13.0.0", "storybook-addon-sass-postcss": "^0.3.2",
"stylelint-order": "^6.0.3", "storybook-solidjs": "^1.0.0-beta.2",
"stylelint-scss": "^6.1.0", "storybook-solidjs-vite": "^1.0.0-beta.2",
"swiper": "11.0.5", "stylelint": "^16.9.0",
"throttle-debounce": "5.0.0", "stylelint-config-recommended": "^14.0.1",
"typescript": "5.2.2", "stylelint-config-standard-scss": "^13.1.0",
"typograf": "7.3.0", "stylelint-order": "^6.0.4",
"uniqolor": "1.1.0", "stylelint-scss": "^6.7.0",
"vike": "0.4.148", "swiper": "^11.1.14",
"vite": "5.1.2", "throttle-debounce": "^5.0.2",
"vite-plugin-mkcert": "^1.17.3", "tslib": "^2.7.0",
"vite-plugin-sass-dts": "^1.3.17", "typescript": "^5.6.2",
"vite-plugin-solid": "2.10.1", "typograf": "^7.4.1",
"y-prosemirror": "1.2.2", "uniqolor": "^1.1.1",
"yjs": "13.6.12" "vinxi": "^0.4.3",
"vite-plugin-mkcert": "^1.17.6",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-sass-dts": "^1.3.29",
"y-prosemirror": "1.2.12",
"yjs": "13.6.19"
}, },
"overrides": { "overrides": {
"y-prosemirror": "1.2.2", "sass": "1.77.6",
"yjs": "13.6.12" "vite": "5.3.5",
"yjs": "13.6.19",
"y-prosemirror": "1.2.12"
},
"engines": {
"node": ">= 20"
},
"trustedDependencies": ["@biomejs/biome", "@swc/core", "esbuild", "protobufjs"],
"dependencies": {
"form-data": "^4.0.0",
"idb": "^8.0.0",
"mailgun.js": "^10.2.3"
} }
} }

View File

@ -10,44 +10,40 @@ import { defineConfig, devices } from '@playwright/test'
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
/* Directory to search for tests */
testDir: './tests', testDir: './tests',
/* Run tests in files in parallel */ /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: 'list',
/* Timeout for each test */
timeout: 40000,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000', baseURL: process.env.BASE_URL || 'https://localhost:3000',
/* Headless */
headless: true,
/* Ignode SSL certificates */
ignoreHTTPSErrors: true,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry' trace: 'on-first-retry'
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{ {
name: 'webkit', name: 'webkit',
use: { ...devices['Desktop Safari'] } use: { ...devices['Desktop Safari'] }
} }
/* Test against mobile viewports. */ /* Test against many viewports.
// { // {
// name: 'Mobile Chrome', // name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] }, // use: { ...devices['Pixel 5'] },
@ -66,12 +62,17 @@ export default defineConfig({
// name: 'Google Chrome', // name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// }, // },
] ],
/* Run your local dev server before starting the tests */ /* Run local dev server before starting the tests */
// webServer: { /* If process env CI is set to false */
// command: 'npm run start', webServer: process.env.CI
// url: 'http://127.0.0.1:3000', ? undefined
// reuseExistingServer: !process.env.CI, : {
// }, command: 'npm run dev',
url: 'http://localhost:3000',
ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI,
timeout: 5 * 60 * 1000
}
}) })

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.125 12.75H4.5C4.08854 12.75 3.75 12.4115 3.75 12C3.75 11.5885 4.08854 11.25 4.5 11.25H19.125C19.5365 11.25 19.875 11.5885 19.875 12C19.875 12.4115 19.5365 12.75 19.125 12.75Z" fill="currentColor"/>
<path
d="M14.0678 18.3593C13.8803 18.3593 13.6928 18.2916 13.547 18.151C13.2501 17.8593 13.2397 17.3853 13.5314 17.0885L18.4584 11.9999L13.5314 6.91137C13.2397 6.6145 13.2501 6.14054 13.547 5.84887C13.8439 5.56241 14.3178 5.57283 14.6043 5.8697L20.0366 11.4791C20.3178 11.7707 20.3178 12.2291 20.0366 12.5207L14.6043 18.1301C14.4584 18.2864 14.2657 18.3593 14.0678 18.3593Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View File

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 714 B

View File

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

View File

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

View File

@ -1,11 +0,0 @@
<svg
width="13" height="16"
viewBox="0 0 13 16"
fill="none"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path
d="M 10.1573,7.43667 C 11.2197,6.70286 11.9645,5.49809 11.9645,4.38095 11.9645,1.90571 10.0478,0 7.58352,0 H 0.738281 V 15.3333 H 8.44876 c 2.28904,0 4.06334,-1.8619 4.06334,-4.1509 0,-1.66478 -0.9419,-3.08859 -2.3548,-3.74573 z M 4.02344,2.73828 h 3.28571 c 0.90905,0 1.64286,0.73381 1.64286,1.64286 0,0.90905 -0.73381,1.64286 -1.64286,1.64286 H 4.02344 Z M 4.01629,9.3405869 h 3.87946 c 0.9090501,0 1.6428601,0.7338101 1.6428601,1.6428601 0,0.90905 -0.73381,1.64286 -1.6428601,1.64286 H 4.01629 Z"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 677 B

4
public/icons/expert.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.9967 4.51318C11.5931 4.51318 11.1868 4.59652 10.8118 4.75798L7.9056 6.01058L9.83268 6.81266L11.4056 6.13558C11.5931 6.05225 11.7962 6.01058 11.9993 6.01058C12.2025 6.01058 12.4056 6.05225 12.5931 6.13558L20.5801 9.57829C20.6504 9.60693 20.6947 9.67464 20.6947 9.75016C20.6947 9.82568 20.6504 9.89339 20.5801 9.92204L12.5931 13.3647C12.2181 13.5262 11.7806 13.5262 11.4056 13.3647L3.41862 9.92204C3.34831 9.89339 3.30404 9.82568 3.30404 9.75016C3.30404 9.67464 3.34831 9.60693 3.41862 9.57829L6.47591 8.26058L11.7103 10.4429C11.804 10.4819 11.903 10.5002 11.9993 10.5002C12.291 10.5002 12.5723 10.3283 12.6921 10.0392C12.8509 9.65641 12.6712 9.21631 12.2884 9.05746L8.39258 7.43506L8.39518 7.43246L6.4681 6.63037L2.42643 8.37516C1.87435 8.60954 1.51758 9.1512 1.51758 9.75016C1.51758 10.3491 1.87435 10.8908 2.42643 11.1252L4.87435 12.1825V18.5679C4.64779 18.7371 4.49935 19.008 4.49935 19.3127V20.8127C4.49935 21.3309 4.91862 21.7502 5.43685 21.7502H5.81185C6.33008 21.7502 6.74935 21.3309 6.74935 20.8127V19.3127C6.74935 19.008 6.60091 18.7371 6.37435 18.5679V17.1512C7.42904 17.909 9.2181 18.7502 11.9993 18.7502C15.5384 18.7502 17.4889 17.3856 18.3353 16.5705C18.8379 16.0887 19.1243 15.4064 19.1243 14.6955V12.1825L21.5723 11.1252C22.1243 10.8908 22.4811 10.3491 22.4811 9.75016C22.4811 9.1512 22.1243 8.60954 21.5723 8.37516L13.1868 4.75798C12.8092 4.59652 12.403 4.51318 11.9967 4.51318ZM6.37435 12.8283L10.8118 14.7424C11.1895 14.9064 11.5931 14.9845 11.9993 14.9845C12.4056 14.9845 12.8092 14.9064 13.1868 14.7424L17.6243 12.8283V14.6955C17.6243 15.0002 17.5046 15.2892 17.2962 15.4897C16.6113 16.146 15.015 17.2502 11.9993 17.2502C8.98372 17.2502 7.38737 16.146 6.70247 15.4897C6.49414 15.2892 6.37435 15.0002 6.37435 14.6955V12.8283Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.25 4.125C7.14583 4.125 6.13281 4.6901 5.60937 5.60156C5.40365 5.95573 5.16406 6.53385 5.03125 6.91927C4.91146 7.2474 3.07813 11.349 1.95313 13.8568L1.95833 13.8594C1.66667 14.4349 1.5 15.0755 1.5 15.75C1.5 18.2318 3.6875 20.25 6.375 20.25C9.0625 20.25 11.25 18.2318 11.25 15.75V14.3724C11.4505 14.3099 11.7109 14.25 12 14.25C12.2891 14.25 12.5495 14.3099 12.75 14.3724V15.75C12.75 18.2318 14.9375 20.25 17.625 20.25C20.3125 20.25 22.5 18.2318 22.5 15.75C22.5 15.0755 22.3333 14.4349 22.0417 13.8594L22.0469 13.8568C20.9219 11.349 19.0885 7.2474 18.9688 6.92448C18.8359 6.53646 18.5964 5.95833 18.3906 5.60417C17.8672 4.6901 16.8542 4.125 15.75 4.125C14.1354 4.125 12.8177 5.32813 12.7552 6.82813C12.526 6.78125 12.2734 6.75 12 6.75C11.7266 6.75 11.474 6.78125 11.2448 6.82813C11.1823 5.32813 9.86458 4.125 8.25 4.125ZM8.25 5.625C9.07813 5.625 9.75 6.21354 9.75 6.9375V12.5104C8.8724 11.7318 7.6849 11.25 6.375 11.25C5.75781 11.25 5.16927 11.362 4.625 11.5547C5.48177 9.64063 6.36458 7.65365 6.45052 7.40885C6.57292 7.04688 6.77604 6.58333 6.90885 6.35156C7.16667 5.90365 7.67969 5.625 8.25 5.625ZM15.75 5.625C16.3203 5.625 16.8333 5.90365 17.0911 6.35156C17.224 6.58333 17.4271 7.04948 17.5495 7.40885C17.6354 7.65365 18.5182 9.64063 19.3724 11.5547C18.8307 11.362 18.2422 11.25 17.625 11.25C16.3151 11.25 15.1276 11.7318 14.25 12.5104V6.9375C14.25 6.21354 14.9219 5.625 15.75 5.625ZM12 8.25C12.2891 8.25 12.5495 8.3099 12.75 8.3724V9.82552C12.5208 9.78125 12.2708 9.75 12 9.75C11.7292 9.75 11.4792 9.78125 11.25 9.82552V8.3724C11.4505 8.3099 11.7109 8.25 12 8.25ZM12 11.25C12.2891 11.25 12.5495 11.3099 12.75 11.3724V12.8255C12.5208 12.7812 12.2708 12.75 12 12.75C11.7292 12.75 11.4792 12.7812 11.25 12.8255V11.3724C11.4505 11.3099 11.7109 11.25 12 11.25ZM6.375 12.75C8.23698 12.75 9.75 14.0964 9.75 15.75C9.75 17.4036 8.23698 18.75 6.375 18.75C4.51302 18.75 3 17.4036 3 15.75C3 14.0964 4.51302 12.75 6.375 12.75ZM17.625 12.75C19.487 12.75 21 14.0964 21 15.75C21 17.4036 19.487 18.75 17.625 18.75C15.763 18.75 14.25 17.4036 14.25 15.75C14.25 14.0964 15.763 12.75 17.625 12.75Z" fill="#141414"/> <path d="M8.625 4.5C7.59115 4.5 6.75 5.34115 6.75 6.375V8.25H5.625C4.59115 8.25 3.75 9.09115 3.75 10.125V17.25C3.75 18.4896 4.76042 19.5 6 19.5H18C19.2396 19.5 20.25 18.4896 20.25 17.25V6.375C20.25 5.34115 19.4089 4.5 18.375 4.5H8.625ZM8.625 6H18.375C18.5807 6 18.75 6.16927 18.75 6.375V17.25C18.75 17.6641 18.4141 18 18 18H8.1224C8.20313 17.7656 8.25 17.513 8.25 17.25V6.375C8.25 6.16927 8.41927 6 8.625 6ZM10.125 7.5C9.71094 7.5 9.375 7.83594 9.375 8.25C9.375 8.66406 9.71094 9 10.125 9H16.875C17.2891 9 17.625 8.66406 17.625 8.25C17.625 7.83594 17.2891 7.5 16.875 7.5H10.125ZM5.625 9.75H6.75V17.25C6.75 17.6641 6.41406 18 6 18C5.58594 18 5.25 17.6641 5.25 17.25V10.125C5.25 9.91927 5.41927 9.75 5.625 9.75ZM10.125 10.125C9.71094 10.125 9.375 10.4609 9.375 10.875C9.375 11.2891 9.71094 11.625 10.125 11.625H16.875C17.2891 11.625 17.625 11.2891 17.625 10.875C17.625 10.4609 17.2891 10.125 16.875 10.125H10.125ZM10.125 12.75C9.71094 12.75 9.375 13.0859 9.375 13.5V16.125C9.375 16.5391 9.71094 16.875 10.125 16.875H12.375C12.7891 16.875 13.125 16.5391 13.125 16.125V13.5C13.125 13.0859 12.7891 12.75 12.375 12.75H10.125ZM15 12.75C14.5859 12.75 14.25 13.0859 14.25 13.5C14.25 13.9141 14.5859 14.25 15 14.25H16.875C17.2891 14.25 17.625 13.9141 17.625 13.5C17.625 13.0859 17.2891 12.75 16.875 12.75H15ZM15 15.375C14.5859 15.375 14.25 15.7109 14.25 16.125C14.25 16.5391 14.5859 16.875 15 16.875H16.875C17.2891 16.875 17.625 16.5391 17.625 16.125C17.625 15.7109 17.2891 15.375 16.875 15.375H15Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

4
public/icons/logout.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16.1785 3.05371C15.1421 3.05371 14.3035 3.89486 14.3035 4.92871C14.3035 5.96256 15.1421 6.80371 16.1785 6.80371C17.215 6.80371 18.0535 5.96256 18.0535 4.92871C18.0535 3.89486 17.215 3.05371 16.1785 3.05371ZM14.6785 7.55371C14.4051 7.55371 14.1473 7.61621 13.9129 7.72038L10.9181 9.12923C10.7723 9.19694 10.6577 9.31413 10.5926 9.45736L9.12124 12.7308C9.07957 12.8089 9.05353 12.8975 9.05353 12.9912C9.05353 13.3011 9.30613 13.5537 9.61603 13.5537C9.8478 13.5537 10.0483 13.4131 10.1343 13.2126V13.21L11.702 10.5303L12.4858 10.249L11.7462 12.6761L11.7541 12.6787C11.7098 12.8376 11.6785 13.0042 11.6785 13.1787C11.6785 13.8923 12.0822 14.5068 12.6707 14.8245L12.6655 14.8298L15.5848 16.9626L16.5874 20.5225H16.59C16.6837 20.8298 16.965 21.0537 17.3035 21.0537C17.7176 21.0537 18.0535 20.7178 18.0535 20.3037C18.0535 20.2282 18.0379 20.1553 18.0171 20.085H18.0197L17.2671 16.3454C17.2436 16.223 17.1968 16.1058 17.1317 15.999L15.4806 13.2881L16.4806 9.99902L16.4572 9.99121C16.5145 9.81152 16.5535 9.62663 16.5535 9.42871C16.5535 8.39486 15.715 7.55371 14.6785 7.55371ZM17.1681 10.5355L16.603 12.0771L17.0353 12.4001C17.0718 12.4261 17.1108 12.4469 17.1525 12.4626L19.8869 13.5042C19.8973 13.5094 19.9103 13.512 19.9207 13.5173L19.9363 13.5225C19.9936 13.5407 20.0535 13.5537 20.116 13.5537C20.4259 13.5537 20.6785 13.3011 20.6785 12.9912C20.6785 12.7699 20.5483 12.5771 20.3608 12.486L17.9233 11.21L17.1681 10.5355ZM8.91551 13.9313C8.69676 13.9105 8.47801 14.0225 8.36863 14.2282L7.43895 15.9886L6.11343 15.2829C5.74884 15.0876 5.29572 15.2256 5.1004 15.5928L3.33738 18.9053C3.14468 19.2673 3.2853 19.723 3.64988 19.9183L4.48582 20.3636C4.47801 20.1058 4.5379 19.8454 4.66551 19.611C4.92593 19.1188 5.43374 18.8115 5.99103 18.8115C6.23322 18.8115 6.47801 18.874 6.69155 18.9886C6.80353 19.0485 6.9077 19.1188 6.99884 19.21L7.98843 17.348C7.99103 17.3454 7.99103 17.3454 7.99363 17.3428L8.2254 16.9027L8.43113 16.5173V16.5146L9.36343 14.7542C9.50926 14.4782 9.40249 14.1396 9.12905 13.9938C9.06134 13.9574 8.98843 13.9365 8.91551 13.9313ZM11.8608 15.3532L11.4988 17.0225L9.93113 19.8844L9.89988 19.9417C9.83999 20.0485 9.80353 20.1709 9.80353 20.3037C9.80353 20.7178 10.1395 21.0537 10.5535 21.0537C10.8244 21.0537 11.0613 20.9079 11.1916 20.6917L13.7332 16.7334L11.8608 15.3532ZM6.05613 19.5641C5.76447 19.5407 5.4728 19.6865 5.32697 19.96C5.13165 20.3271 5.27228 20.7803 5.63686 20.9756C6.00145 21.1683 6.45718 21.0303 6.65249 20.6657C6.8452 20.2985 6.70718 19.8454 6.33999 19.6501C6.24884 19.6006 6.15249 19.5745 6.05613 19.5641Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

4
public/icons/profile.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.4285 3.05347C11.392 3.05347 10.5535 3.89461 10.5535 4.92847C10.5535 5.96232 11.392 6.80347 12.4285 6.80347C13.4649 6.80347 14.3035 5.96232 14.3035 4.92847C14.3035 3.89461 13.4649 3.05347 12.4285 3.05347ZM12.4285 7.55347C10.3113 7.55347 9.05347 9.05347 9.05347 10.1785V14.6785C9.05347 15.0925 9.3894 15.4285 9.80347 15.4285H10.1785V21.7852C10.1785 22.2097 10.5222 22.5535 10.9467 22.5535C11.3582 22.5535 11.6941 22.2332 11.7149 21.8243L12.017 15.4285H12.8399L13.142 21.8243C13.1628 22.2332 13.4988 22.5535 13.9102 22.5535C14.3347 22.5535 14.6785 22.2097 14.6785 21.7852V15.4285H15.0535C15.4675 15.4285 15.8035 15.0925 15.8035 14.6785V10.1785C15.8035 9.05347 14.5457 7.55347 12.4285 7.55347Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="10" viewBox="0 0 18 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.1042 9.90633C16.9063 9.90633 16.7136 9.82821 16.5626 9.67716L8.98965 1.91675L1.43236 9.66154C1.1459 9.95841 0.671944 9.96362 0.375069 9.67716C0.0781948 9.3855 0.0729868 8.91154 0.359444 8.61467L8.45319 0.322998C8.73965 0.0313314 9.24486 0.0313314 9.53132 0.322998L17.6407 8.63029C17.9272 8.92716 17.9219 9.40112 17.6251 9.69279C17.4792 9.83342 17.2917 9.90633 17.1042 9.90633Z" fill="#9FA1A7"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@ -1,527 +0,0 @@
{
"A guide to horizontal editorial: how an open journal works": "A guide to horizontal editorial: how an open journal works",
"About the project": "About the project",
"About": "About",
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title",
"Add a link or click plus to embed media": "Add a link or click plus to embed media",
"Add an embed widget": "Add an embed widget",
"Add another image": "Add another image",
"Add audio": "Add audio",
"Add blockquote": "Add blockquote",
"Add comment": "Comment",
"Add cover": "Add cover",
"Add image": "Add image",
"Add images": "Add images",
"Add intro": "Add intro",
"Add link": "Add link",
"Add rule": "Add rule",
"Add signature": "Add signature",
"Add subtitle": "Add subtitle",
"Add url": "Add url",
"Add": "Add",
"Address on Discours": "Address on Discours",
"Album name": "Название aльбома",
"Alignment center": "Alignment center",
"Alignment left": "Alignment left",
"Alignment right": "Alignment right",
"All articles": "All articles",
"All authors": "All authors",
"All posts": "All posts",
"All topics": "All topics",
"All": "All",
"Almost done! Check your email.": "Almost done! Just checking your email.",
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
"Are you sure you want to delete this draft?": "Are you sure you want to delete this draft?",
"Are you sure you want to to proceed the action?": "Are you sure you want to to proceed the action?",
"Art": "Art",
"Artist": "Artist",
"Artworks": "Artworks",
"Audio": "Audio",
"Author": "Author",
"Authors": "Authors",
"Autotypograph": "Autotypograph",
"Back to editor": "Back to editor",
"Back to main page": "Back to main page",
"Back": "Back",
"Be the first to rate": "Be the first to rate",
"Become an author": "Become an author",
"Bold": "Bold",
"Bookmarked": "Saved",
"Bookmarks": "Bookmarks",
"Bullet list": "Bullet list",
"By alphabet": "By alphabet",
"By authors": "By authors",
"By name": "By name",
"By popularity": "By popularity",
"By rating": "By popularity",
"By relevance": "By relevance",
"By shouts": "By publications",
"By signing up you agree with our": "By signing up you agree with our",
"By time": "By time",
"By title": "By title",
"By updates": "By updates",
"By views": "By views",
"Can make any changes, accept or reject suggestions, and share access with others": "Can make any changes, accept or reject suggestions, and share access with others",
"Can offer edits and comments, but cannot edit the post or share access with others": "Can offer edits and comments, but cannot edit the post or share access with others",
"Can write and edit text directly, and accept or reject suggestions from others": "Can write and edit text directly, and accept or reject suggestions from others",
"Cancel changes": "Cancel changes",
"Cancel": "Cancel",
"Change password": "Change password",
"Characters": "Знаков",
"Chat Title": "Chat Title",
"Choose a post type": "Choose a post type",
"Choose a title image for the article. You can immediately see how the publication card will look like.": "Choose a title image for the article. You can immediately see how the publication card will look like.",
"Choose who you want to write to": "Choose who you want to write to",
"Close": "Close",
"Co-author": "Co-author",
"Collaborate": "Help Edit",
"Collaborators": "Collaborators",
"Collections": "Collections",
"Come up with a subtitle for your story": "Come up with a subtitle for your story",
"Come up with a title for your story": "Come up with a title for your story",
"Coming soon": "Coming soon",
"Comment successfully deleted": "Comment successfully deleted",
"Commentator": "Commentator",
"Comments": "Comments",
"Communities": "Communities",
"Community Discussion Rules": "Community Discussion Rules",
"Community Principles": "Community Principles",
"Community values and rules of engagement for the open editorial team": "Community values and rules of engagement for the open editorial team",
"Confirm": "Confirm",
"Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom",
"Cooperate": "Cooperate",
"Copy link": "Copy link",
"Copy": "Copy",
"Corrections history": "Corrections history",
"Create Chat": "Create Chat",
"Create Group": "Create a group",
"Create account": "Create an account",
"Create an account to add to your bookmarks": "Create an account to add to your bookmarks",
"Create an account to participate in discussions": "Create an account to participate in discussions",
"Create an account to publish articles": "Create an account to publish articles",
"Create an account to subscribe to new publications": "Create an account to subscribe to new publications",
"Create an account to subscribe": "Create an account to subscribe",
"Create an account to vote": "Create an account to vote",
"Create gallery": "Create gallery",
"Create post": "Create post",
"Create video": "Create video",
"Crop image": "Crop image",
"Culture": "Culture",
"Date of Birth": "Date of Birth",
"Decline": "Decline",
"Delete cover": "Delete cover",
"Delete userpic": "Delete userpic",
"Delete": "Delete",
"Description": "Description",
"Discours Manifest": "Discours Manifest",
"Discours Partners": "Discours Partners",
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is&#160;an&#160;intellectual environment, a&#160;web space and tools that allows authors to&#160;collaborate with readers and come together to&#160;co-create publications and media projects.<br/><em>We&#160;are convinced that one voice is&#160;good, but many is&#160;better. We&#160;create the most amazing stories together</em>",
"Discours is created with our common effort": "Discours exists because of our common effort",
"Discours an open magazine about culture, science and society": "Discours an open magazine about culture, science and society",
"Discours": "Discours",
"Discussing": "Discussing",
"Discussion rules": "Discussion rules",
"Discussions": "Discussions",
"Do you really want to reset all changes?": "Do you really want to reset all changes?",
"Dogma": "Dogma",
"Draft successfully deleted": "Draft successfully deleted",
"Drafts": "Drafts",
"Drag the image to this area": "Drag the image to this area",
"Each image must be no larger than 5 MB.": "Each image must be no larger than 5 MB.",
"Edit profile": "Edit profile",
"Edit": "Edit",
"Editing": "Editing",
"Editor": "Editor",
"Email": "Mail",
"Enter URL address": "Enter URL address",
"Enter a new password": "Enter a new password",
"Enter footnote text": "Enter footnote text",
"Enter image description": "Enter image description",
"Enter image title": "Enter image title",
"Enter text": "Enter text",
"Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration",
"Enter your new password": "Enter your new password",
"Enter": "Enter",
"Error": "Error",
"Please give us your email address": "Please provide us your email address to get the password reset link",
"Experience": "Experience",
"FAQ": "Tips and suggestions",
"Favorite topics": "Favorite topics",
"Favorite": "Favorites",
"Feed settings": "Feed settings",
"Feed": "Feed",
"Feedback": "Feedback",
"Fill email": "Fill email",
"Fixed": "Fixed",
"Follow the topic": "Follow the topic",
"Follow": "Follow",
"Followers": "Followers",
"Following": "Following",
"Forward": "Forward",
"Full name": "First and last name",
"Gallery name": "Gallery name",
"Gallery": "Gallery",
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine",
"Go to main page": "Go to main page",
"Group Chat": "Group Chat",
"Groups": "Groups",
"Header 1": "Header 1",
"Header 2": "Header 2",
"Header 3": "Header 3",
"Headers": "Headers",
"Help to edit": "Help to edit",
"Help": "Помощь",
"Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.",
"Here you can manage all your Discours subscriptions": "Here you can manage all your Discours subscriptions",
"Here you can upload your photo": "Here you can upload your photo",
"Hide table of contents": "Hide table of contents",
"Highlight": "Highlight",
"Hooray! Welcome!": "Hooray! Welcome!",
"Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform",
"Hot topics": "Hot topics",
"Hotkeys": "Горячие клавиши",
"How Discours works": "How Discours works",
"How can I help/skills": "How can I help/skills",
"How it works": "How it works",
"How to help": "How to help?",
"How to write a good article": "Как написать хорошую статью",
"How to write an article": "How to write an article",
"Hundreds of people from different countries and cities share their knowledge and art on the Discours. Join us!": "Hundreds of people from different countries and cities share their knowledge and art on the Discours. Join us!",
"I have an account": "I have an account!",
"I have no account yet": "I don't have an account yet",
"I know the password": "I know the password",
"Image format not supported": "Image format not supported",
"In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to": "In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to",
"Inbox": "Inbox",
"Incut": "Incut",
"Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society",
"Independent media project about culture, science, art and society with horizontal editing": "Independent media project about culture, science, art and society with horizontal editing",
"Insert footnote": "Insert footnote",
"Insert video link": "Insert video link",
"Interview": "Interview",
"Introduce": "Introduction",
"Invalid email": "Check if your email is correct",
"Invalid image URL": "Invalid image URL",
"Invalid url format": "Invalid url format",
"Invite co-authors": "Invite co-authors",
"Invite collaborators": "Invite collaborators",
"Invite to collab": "Invite to Collab",
"Invite": "Invite",
"It does not look like url": "It doesn't look like a link",
"Italic": "Italic",
"Join our maillist": "To receive the best postings, just enter your email",
"Join the community": "Join the community",
"Join the global community of authors!": "Join the global community of authors from all over the world!",
"Join": "Join",
"Just start typing...": "Just start typing...",
"Knowledge base": "Knowledge base",
"Language": "Language",
"Last rev.": "Посл. изм.",
"Let's log in": "Let's log in",
"Link copied to clipboard": "Link copied to clipboard",
"Link copied": "Link copied",
"Link sent, check your email": "Link sent, check your email",
"List of authors of the open editorial community": "List of authors of the open editorial community",
"Lists": "Lists",
"Literature": "Literature",
"Load more": "Show more",
"Loading": "Loading",
"Logout": "Logout",
"Looks like you forgot to upload the video": "Looks like you forgot to upload the video",
"Manifest of samizdat: principles and mission of an open magazine with a horizontal editorial board": "Manifest of samizdat: principles and mission of an open magazine with a horizontal editorial board",
"Manifesto": "Manifesto",
"Many files, choose only one": "Many files, choose only one",
"Mark as read": "Mark as read",
"Material card": "Material card",
"Message": "Message",
"More": "More",
"Most commented": "Commented",
"Most read": "Readable",
"Move down": "Move down",
"Move up": "Move up",
"Music": "Music",
"My feed": "My feed",
"My subscriptions": "Subscriptions",
"Name": "Name",
"New literary work": "New literary work",
"New only": "New only",
"New password": "New password",
"New stories every day and even more!": "New stories and more are waiting for you every day!",
"Newsletter": "Newsletter",
"Night mode": "Night mode",
"No notifications yet": "No notifications yet",
"Nothing here yet": "There's nothing here yet",
"Nothing is here": "There is nothing here",
"Notifications": "Notifications",
"Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password": "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password",
"Or paste a link to an image": "Or paste a link to an image",
"Ordered list": "Ordered list",
"Our regular contributor": "Our regular contributor",
"Paragraphs": "Абзацев",
"Participate in the Discours: share information, join the editorial team": "Участвуйте в Дискурсе: делитесь информацией, присоединяйтесь к редакции",
"Participating": "Participating",
"Participation": "Participation",
"Partners": "Partners",
"Password again": "Password again",
"Password should be at least 8 characters": "Password should be at least 8 characters",
"Password should contain at least one number": "Password should contain at least one number",
"Password should contain at least one special character: !@#$%^&*": "Password should contain at least one special character: !@#$%^&*",
"Password updated!": "Password updated!",
"Password": "Password",
"Passwords are not equal": "Passwords are not equal",
"Paste Embed code": "Paste Embed code",
"Personal": "Personal",
"Pin": "Pin",
"Platform Guide": "Platform Guide",
"Please check your email address": "Please check your email address",
"Please confirm your email to finish": "Confirm your email and the action will complete",
"Please enter a name to sign your comments and publication": "Please enter a name to sign your comments and publication",
"Please enter email": "Please enter your email",
"Please enter password again": "Please enter password again",
"Please enter password": "Please enter a password",
"Please, confirm email": "Please confirm email",
"Please, set the main topic first": "Please, set the main topic first",
"Podcasts": "Podcasts",
"Poetry": "Poetry",
"Popular authors": "Popular authors",
"Popular": "Popular",
"Principles": "Community principles",
"Professional principles that the open editorial team follows in its work": "Professional principles that the open editorial team follows in its work",
"Profile settings": "Profile settings",
"Profile": "Profile",
"Publications": "Publications",
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}",
"FollowersWithCount": "{count, plural, =0 {no followers} one {{count} follower} other {{count} followers}}",
"Publish Album": "Publish Album",
"Publish Settings": "Publish Settings",
"Published": "Published",
"Punchline": "Punchline",
"Quit": "Quit",
"Quote": "Quote",
"Quotes": "Quotes",
"Reason uknown": "Reason unknown",
"Recent": "Fresh",
"Registered since {date}": "Registered since {date}",
"Remove link": "Remove link",
"Reply": "Reply",
"Report": "Complain",
"Reports": "Reports",
"Required": "Required",
"Resend code": "Send confirmation",
"Rules of the journal Discours": "Rules of the journal Discours",
"Save draft": "Save draft",
"Save settings": "Save settings",
"Saving...": "Saving...",
"Scroll up": "Scroll up",
"Search author": "Search author",
"Search topic": "Search topic",
"Search": "Search",
"Sections": "Sections",
"Security": "Security",
"Select": "Select",
"Self-publishing exists thanks to the help of wonderful people from all over the world. Thank you!": "Samizdat exists thanks to the help of wonderful people from all over the world. Thank you!",
"Send link again": "Send link again",
"Send": "Send",
"Set the new password": "Set the new password",
"Settings": "Settings",
"Share publication": "Share publication",
"Share": "Share",
"Show lyrics": "Show lyrics",
"Show more": "Show more",
"Show table of contents": "Show table of contents",
"Show": "Show",
"Site search": "Site search",
"Slug": "Slug",
"Social networks": "Social networks",
"Society": "Society",
"Some new comments to your publication": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication",
"Some new replies to your comment": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication",
"Something went wrong, check email and password": "Something went wrong. Check your email and password",
"Something went wrong, please try again": "Something went wrong, please try again",
"Song lyrics": "Song lyrics...",
"Song title": "Song title",
"Soon": "Скоро",
"Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one",
"Special Projects": "Special Projects",
"Special projects": "Special projects",
"Specify the source and the name of the author": "Specify the source and the name of the author",
"Start conversation": "Start a conversation",
"Start dialog": "Start dialog",
"Subsccriptions": "Subscriptions",
"Subscribe to the best publications newsletter": "Subscribe to the best publications newsletter",
"Subscribe us": "Subscribe us",
"Subscribe what you like to tune your personal feed": "Subscribe to topics that interest you to customize your personal feed and get instant updates on new posts and discussions",
"Subscribe who you like to tune your personal feed": "Subscribe to authors you're interested in to customize your personal feed and get instant updates on new posts and discussions",
"Subscribe": "Subscribe",
"SubscriberWithCount": "{count, plural, =0 {no followers} one {{count} follower} other {{count} followers}}",
"Subscription": "Subscription",
"SubscriptionWithCount": "{count, plural, =0 {no subscriptions} one {{count} subscription} other {{count} subscriptions}}",
"Subscriptions": "Subscriptions",
"Substrate": "Substrate",
"Success": "Success",
"Successfully authorized": "Authorization successful",
"Suggest an idea": "Suggest an idea",
"Support Discours": "Support Discours",
"Support the project": "Support the project",
"Support us": "Support us",
"Terms of use": "Site rules",
"Text checking": "Text checking",
"Thank you!": "Thank you!",
"Thank you": "Thank you",
"The address is already taken": "The address is already taken",
"The most interesting publications on the topic": "The most interesting publications on the topic {topicName}",
"Thematic table of contents of the magazine. Here you can find all the topics that community authors have written about.": "Thematic table of contents of the magazine. Here you can find all the topics that community authors have written about.",
"Thematic table of contents of the magazine. Here you can find all the topics that the community authors wrote about": "Thematic table of contents of the magazine. Here you can find all the topics that the community authors wrote about",
"Themes and plots": "Themes and plots",
"Please, set the article title": "Please, set the article title",
"Theory": "Theory",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?",
"This comment has not yet been rated": "This comment has not yet been rated",
"This email is": "This email is",
"This email is not verified": "This email is not verified",
"This email is verified": "This email is verified",
"This email is registered": "This email is registered",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "This functionality is currently not available, we would like to work on this issue. Use the download link.",
"This month": "This month",
"This post has not been rated yet": "This post has not been rated yet",
"This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted": "This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted",
"This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed": "This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed",
"This week": "This week",
"This year": "This year",
"To find publications, art, comments, authors and topics of interest to you, just start typing your query": "To&nbsp;find publications, art, comments, authors and topics of&nbsp;interest to&nbsp;you, just start typing your query",
"To leave a comment please": "To leave a comment please",
"To write a comment, you must": "To write a comment, you must",
"Top authors": "Authors rating",
"Top commented": "Most commented",
"Top discussed": "Top discussed",
"Top month articles": "Top of the month",
"Top rated": "Popular",
"Top recent": "Most recent",
"Top topics": "Interesting topics",
"Top viewed": "Most viewed",
"Topic is supported by": "Topic is supported by",
"Topics which supported by author": "Topics which supported by author",
"Topics": "Topics",
"Try to find another way": "Try to find another way",
"Unfollow the topic": "Unfollow the topic",
"Unfollow": "Unfollow",
"Unnamed draft": "Unnamed draft",
"Upload error": "Upload error",
"Upload userpic": "Upload userpic",
"Upload video": "Upload video",
"Upload": "Upload",
"Uploading image": "Uploading image",
"Username": "Username",
"Userpic": "Userpic",
"Users": "Users",
"Video format not supported": "Video format not supported",
"Video": "Video",
"Views": "Views",
"We are working on collaborative editing of articles and in the near future you will have an amazing opportunity - to create together with your colleagues": "We are working on collaborative editing of articles and in the near future you will have an amazing opportunity - to create together with your colleagues",
"We can't find you, check email or": "We can't find you, check email or",
"We couldn't find anything for your request": "We&nbsp;couldn&rsquo;t find anything for your request",
"We know you, please try to login": "This email address is already registered, please try to login",
"We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.",
"Welcome to Discours to add to your bookmarks": "Welcome to Discours to add to your bookmarks",
"Welcome to Discours to participate in discussions": "Welcome to Discours to participate in discussions",
"Welcome to Discours to publish articles": "Welcome to Discours to publish articles",
"Welcome to Discours to subscribe to new publications": "Welcome to Discours to subscribe to new publications",
"Welcome to Discours to subscribe": "Welcome to Discours to subscribe",
"Welcome to Discours to vote": "Welcome to Discours to vote",
"Welcome to Discours": "Welcome to Discours",
"Where": "From",
"Why you can earn a hole in your karma and how to receive rays of gratitude for your contribution to discussions in samizdat communities": "Why you can earn a hole in your karma and how to receive rays of gratitude for your contribution to discussions in samizdat communities",
"Words": "Слов",
"Work with us": "Cooperate with Discours",
"Write a comment...": "Write a comment...",
"Write a short introduction": "Write a short introduction",
"Write about the topic": "Write about the topic",
"Write an article": "Write an article",
"Write comment": "Write comment",
"Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here",
"Write message": "Write a message",
"Write to us": "Write to us",
"You can": "You can",
"Write your colleagues name or email": "Write your colleague's name or email",
"You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats",
"You can now login using your new password": "Теперь вы можете входить с помощью нового пароля",
"You were successfully authorized": "You were successfully authorized",
"You&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;new responses": "You&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;new responses",
"You've confirmed email": "You've confirmed email",
"You've reached a non-existed page": "You've reached a non-existed page",
"Your email": "Your email",
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses",
"actions": "actions",
"add link": "add link",
"all topics": "all topics",
"and some more authors": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}",
"article": "article",
"author": "author",
"authors": "authors",
"authorsWithCount": "{count} {count, plural, one {author} other {authors}}",
"back to menu": "back to menu",
"bold": "bold",
"bookmarks": "bookmarks",
"cancel": "cancel",
"collections": "collections",
"community": "community",
"contents": "contents",
"delimiter": "delimiter",
"discussion": "Discours",
"dogma keywords": "Discours.io, dogma, editorial principles, code of ethics, journalism, community",
"drafts": "drafts",
"earlier": "earlier",
"email not confirmed": "email not confirmed",
"enter": "enter",
"feed": "feed",
"follower": "follower",
"followersWithCount": "{count} {count, plural, one {follower} other {followers}}",
"from": "from",
"header 1": "header 1",
"header 2": "header 2",
"header 3": "header 3",
"images": "images",
"invalid password": "invalid password",
"italic": "italic",
"journal": "journal",
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
"keywords": "Discours.io, Discours magazine, Discours, culture, science, art, society, independent journalism, literature, music, cinema, video, photography",
"literature": "literature",
"marker list": "marker list",
"min. 1400×1400 pix": "мин. 1400×1400 пикс.",
"music": "music",
"my feed": "my ribbon",
"not verified": "not verified",
"number list": "number list",
"or sign in with social networks": "or sign in with social networks",
"personal data usage and email notifications": "to process personal data and receive email notifications",
"post": "post",
"principles keywords": "Discours.io, communities, values, editorial rules, polyphony, creation",
"register": "register",
"registered": "registered",
"repeat": "repeat",
"resend confirmation link": "resend confirmation link",
"shout": "post",
"shoutsWithCount": "{count} {count, plural, one {post} other {posts}}",
"sign up or sign in": "sign up or sign in",
"slug is used by another user": "Slug is already taken by another user",
"subscriber": "subscriber",
"subscriber_rp": "subscriber",
"subscribers": "subscribers",
"subscribing...": "subscribing...",
"subscription": "subscription",
"subscription_rp": "subscription",
"subscriptions": "subscriptions",
"terms of use keywords": "Discours.io, site rules, terms of use",
"terms of use": "terms of use",
"today": "today",
"topicKeywords": "{topic}, Discours.io, articles, journalism, research",
"topics": "topics",
"user already exist": "user already exists",
"verified": "verified",
"video": "video",
"view": "view",
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
"yesterday": "yesterday"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -1,2 +1,2 @@
User-agent: * User-agent: *
Allow: / Disallow: /

51
src/app.tsx Normal file
View File

@ -0,0 +1,51 @@
import { Meta, MetaProvider } from '@solidjs/meta'
import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { type JSX, Suspense } from 'solid-js'
import { AuthToken } from '@authorizerdev/authorizer-js'
import { Loading } from './components/_shared/Loading'
import { AuthorsProvider } from './context/authors'
import { EditorProvider } from './context/editor'
import { FeedProvider } from './context/feed'
import { LocalizeProvider } from './context/localize'
import { SessionProvider } from './context/session'
import { TopicsProvider } from './context/topics'
import { UIProvider } from './context/ui'
import '~/styles/app.scss'
export const Providers = (props: { children?: JSX.Element }) => {
const sessionStateChanged = (payload: AuthToken) => {
console.debug(payload)
// TODO: maybe load subs here
}
return (
<LocalizeProvider>
<SessionProvider onStateChangeCallback={sessionStateChanged}>
<TopicsProvider>
<FeedProvider>
<MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<UIProvider>
<EditorProvider>
<AuthorsProvider>
<Suspense fallback={<Loading />}>{props.children}</Suspense>
</AuthorsProvider>
</EditorProvider>
</UIProvider>
</MetaProvider>
</FeedProvider>
</TopicsProvider>
</SessionProvider>
</LocalizeProvider>
)
}
export const App = () => (
<Router root={Providers}>
<FileRoutes />
</Router>
)
export default App

View File

@ -1,143 +0,0 @@
import type { PageProps, RootSearchParams } from '../pages/types'
import { Meta, MetaProvider } from '@solidjs/meta'
import { Component, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web'
import { ConfirmProvider } from '../context/confirm'
import { ConnectProvider } from '../context/connect'
import { EditorProvider } from '../context/editor'
import { FollowingProvider } from '../context/following'
import { InboxProvider } from '../context/inbox'
import { LocalizeProvider } from '../context/localize'
import { MediaQueryProvider } from '../context/mediaQuery'
import { NotificationsProvider } from '../context/notifications'
import { SessionProvider } from '../context/session'
import { SnackbarProvider } from '../context/snackbar'
import { DiscussionRulesPage } from '../pages/about/discussionRules.page'
import { DogmaPage } from '../pages/about/dogma.page'
import { GuidePage } from '../pages/about/guide.page'
import { HelpPage } from '../pages/about/help.page'
import { ManifestPage } from '../pages/about/manifest.page'
import { PartnersPage } from '../pages/about/partners.page'
import { PrinciplesPage } from '../pages/about/principles.page'
import { ProjectsPage } from '../pages/about/projects.page'
import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
import { ThanksPage } from '../pages/about/thanks.page'
import { AllAuthorsPage } from '../pages/allAuthors.page'
import { AllTopicsPage } from '../pages/allTopics.page'
import { ArticlePage } from '../pages/article.page'
import { AuthorPage } from '../pages/author.page'
import { ConnectPage } from '../pages/connect.page'
import { CreatePage } from '../pages/create.page'
import { DraftsPage } from '../pages/drafts.page'
import { EditPage } from '../pages/edit.page'
import { ExpoPage } from '../pages/expo/expo.page'
import { FeedPage } from '../pages/feed.page'
import { FourOuFourPage } from '../pages/fourOuFour.page'
import { InboxPage } from '../pages/inbox.page'
import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router'
import { MODALS, hideModal, showModal } from '../stores/ui'
// TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage'))
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
author: AuthorPage,
authorComments: AuthorPage,
authorAbout: AuthorPage,
inbox: InboxPage,
expo: ExpoPage,
connect: ConnectPage,
create: CreatePage,
edit: EditPage,
editSettings: EditPage,
drafts: DraftsPage,
home: HomePage,
topics: AllTopicsPage,
topic: TopicPage,
authors: AllAuthorsPage,
feed: FeedPage,
feedMy: FeedPage,
feedNotifications: FeedPage,
feedBookmarks: FeedPage,
feedCollaborations: FeedPage,
feedDiscussions: FeedPage,
article: ArticlePage,
search: SearchPage,
discussionRules: DiscussionRulesPage,
dogma: DogmaPage,
guide: GuidePage,
help: HelpPage,
manifest: ManifestPage,
projects: ProjectsPage,
partners: PartnersPage,
principles: PrinciplesPage,
termsOfUse: TermsOfUsePage,
thanks: ThanksPage,
profileSettings: ProfileSettingsPage,
profileSecurity: ProfileSecurityPage,
profileSubscriptions: ProfileSubscriptionsPage,
fourOuFour: FourOuFourPage
}
type Props = PageProps & { is404: boolean }
export const App = (props: Props) => {
const { page, searchParams } = useRouter<RootSearchParams>()
const is404 = createMemo(() => props.is404)
createEffect(() => {
if (!searchParams().m) {
hideModal()
}
const modal = MODALS[searchParams().m]
if (modal) {
showModal(modal)
}
})
const pageComponent = createMemo(() => {
const result = pagesMap[page()?.route || 'home']
if (is404() || !result || page()?.path === '/404') {
return FourOuFourPage
}
return result
})
return (
<MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<LocalizeProvider>
<MediaQueryProvider>
<SnackbarProvider>
<ConfirmProvider>
<SessionProvider onStateChangeCallback={console.log}>
<FollowingProvider>
<ConnectProvider>
<NotificationsProvider>
<EditorProvider>
<InboxProvider>
<Dynamic component={pageComponent()} {...props} />
</InboxProvider>
</EditorProvider>
</NotificationsProvider>
</ConnectProvider>
</FollowingProvider>
</SessionProvider>
</ConfirmProvider>
</SnackbarProvider>
</MediaQueryProvider>
</LocalizeProvider>
</MetaProvider>
)
}

View File

@ -22,10 +22,21 @@ img {
.articleContent { .articleContent {
img:not([data-disable-lightbox='true']) { img:not([data-disable-lightbox='true']) {
cursor: zoom-in; cursor: zoom-in;
width: 100%;
} }
} }
.shoutBody { .shoutBody {
@include media-breakpoint-up(sm) {
:global(.width-30) {
width: 30%;
}
:global(.width-50) {
width: 50%;
}
}
font-size: 1.6rem; font-size: 1.6rem;
line-height: 1.6; line-height: 1.6;
@ -64,6 +75,16 @@ img {
blockquote[data-type='quote'], blockquote[data-type='quote'],
ta-quotation { ta-quotation {
@include media-breakpoint-up(sm) {
&[data-float='left'] {
margin-right: 1.5em;
}
&[data-float='right'] {
margin-left: 1.5em;
}
}
border: solid #000; border: solid #000;
border-width: 0 0 0 2px; border-width: 0 0 0 2px;
clear: both; clear: both;
@ -77,21 +98,11 @@ img {
&[data-float='right'] { &[data-float='right'] {
@include font-size(2.2rem); @include font-size(2.2rem);
line-height: 1.4;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
clear: none; clear: none;
} }
}
@include media-breakpoint-up(sm) { line-height: 1.4;
&[data-float='left'] {
margin-right: 1.5em;
}
&[data-float='right'] {
margin-left: 1.5em;
}
} }
&::before { &::before {
@ -105,17 +116,17 @@ img {
ta-border-sub { ta-border-sub {
@include font-size(1.4rem); @include font-size(1.4rem);
@include media-breakpoint-up(md) {
margin: 3.2rem -8.3333%;
padding: 3.2rem 8.3333%;
}
background: #f1f2f3; background: #f1f2f3;
clear: both; clear: both;
display: block; display: block;
margin: 3.2rem 0; margin: 3.2rem 0;
padding: 3.2rem; padding: 3.2rem;
@include media-breakpoint-up(md) {
margin: 3.2rem -8.3333%;
padding: 3.2rem 8.3333%;
}
p:last-child { p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -193,16 +204,6 @@ img {
margin: 0 8.3333% 1.5em 0; margin: 0 8.3333% 1.5em 0;
} }
@include media-breakpoint-up(sm) {
:global(.width-30) {
width: 30%;
}
:global(.width-50) {
width: 50%;
}
}
:global(.img-align-left.width-50) { :global(.img-align-left.width-50) {
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
margin-left: -16.6666%; margin-left: -16.6666%;
@ -312,20 +313,24 @@ img {
} }
.shoutStats { .shoutStats {
@include media-breakpoint-down(lg) {
flex-wrap: wrap;
}
border-top: 4px solid #000; border-top: 4px solid #000;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
padding: 3rem 0 0; padding: 3rem 0 0;
position: relative; position: relative;
@include media-breakpoint-down(lg) {
flex-wrap: wrap;
}
} }
.shoutStatsItem { .shoutStatsItem {
@include font-size(1.5rem); @include font-size(1.5rem);
@include media-breakpoint-up(xl) {
margin-right: 3.2rem;
}
align-items: center; align-items: center;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
@ -333,10 +338,6 @@ img {
vertical-align: baseline; vertical-align: baseline;
cursor: pointer; cursor: pointer;
@include media-breakpoint-up(xl) {
margin-right: 3.2rem;
}
.icon { .icon {
display: inline-block; display: inline-block;
margin-right: 0.2em; margin-right: 0.2em;
@ -378,11 +379,11 @@ img {
} }
.shoutStatsItemBookmarks { .shoutStatsItemBookmarks {
margin-left: auto;
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
margin-left: 0; margin-left: 0;
} }
margin-left: auto;
} }
.shoutStatsItemInner { .shoutStatsItemInner {
@ -407,6 +408,15 @@ img {
} }
.shoutStatsItemAdditionalData { .shoutStatsItemAdditionalData {
@include media-breakpoint-down(lg) {
flex: 1 100%;
order: 9;
.shoutStatsItemAdditionalDataItem {
margin-left: 0;
}
}
color: rgb(0 0 0 / 40%); color: rgb(0 0 0 / 40%);
cursor: default; cursor: default;
font-weight: normal; font-weight: normal;
@ -417,24 +427,9 @@ img {
opacity: 0.4; opacity: 0.4;
height: 2rem; height: 2rem;
} }
@include media-breakpoint-down(lg) {
flex: 1 100%;
order: 9;
.shoutStatsItemAdditionalDataItem {
margin-left: 0;
}
}
} }
.shoutStatsItemViews { .shoutStatsItemViews {
color: rgb(0 0 0 / 40%);
cursor: default;
font-weight: normal;
margin-left: auto;
white-space: nowrap;
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
bottom: 0; bottom: 0;
flex: 1 40%; flex: 1 40%;
@ -448,6 +443,12 @@ img {
display: none !important; display: none !important;
} }
} }
color: rgb(0 0 0 / 40%);
cursor: default;
font-weight: normal;
margin-left: auto;
white-space: nowrap;
} }
.shoutStatsItemLabel { .shoutStatsItemLabel {
@ -456,11 +457,11 @@ img {
} }
.commentsTextLabel { .commentsTextLabel {
display: none;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
display: block; display: block;
} }
display: none;
} }
.shoutStatsItemCount { .shoutStatsItemCount {
@ -470,6 +471,12 @@ img {
} }
.shoutStatsItemAdditionalDataItem { .shoutStatsItemAdditionalDataItem {
@include media-breakpoint-down(sm) {
&:first-child {
margin-left: 0;
}
}
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
@ -477,12 +484,6 @@ img {
margin-right: 0; margin-right: 0;
margin-bottom: 0; margin-bottom: 0;
cursor: default; cursor: default;
@include media-breakpoint-down(sm) {
&:first-child {
margin-left: 0;
}
}
} }
.topicsList { .topicsList {

View File

@ -36,7 +36,7 @@
width: 200px; width: 200px;
height: 200px; height: 200px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
background: var(--placeholder-color-semi) url('/icons/create-music.svg') no-repeat 50% 50%; background: var(--placeholder-color-semi) url('/icons/create-audio.svg') no-repeat 50% 50%;
.image { .image {
object-fit: cover; object-fit: cover;

View File

@ -1,11 +1,11 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { Topic } from '../../../graphql/schema/core.gen' import { Icon } from '~/components/_shared/Icon'
import { MediaItem } from '../../../pages/types' import { Image } from '~/components/_shared/Image'
import { Topic } from '~/graphql/schema/core.gen'
import { MediaItem } from '~/types/mediaitem'
import { CardTopic } from '../../Feed/CardTopic' import { CardTopic } from '../../Feed/CardTopic'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import styles from './AudioHeader.module.scss' import styles from './AudioHeader.module.scss'
@ -30,19 +30,19 @@ export const AudioHeader = (props: Props) => {
</div> </div>
<div class={styles.albumInfo}> <div class={styles.albumInfo}>
<Show when={props.topic}> <Show when={props.topic}>
<CardTopic title={props.topic.title} slug={props.topic.slug} /> <CardTopic title={props.topic.title || ''} slug={props.topic.slug} />
</Show> </Show>
<h1>{props.title}</h1> <h1>{props.title}</h1>
<Show when={props.artistData}> <Show when={props.artistData}>
<div class={styles.artistData}> <div class={styles.artistData}>
<Show when={props.artistData?.artist}> <Show when={props.artistData?.artist}>
<div class={styles.item}>{props.artistData.artist}</div> <div class={styles.item}>{props.artistData?.artist || ''}</div>
</Show> </Show>
<Show when={props.artistData?.date}> <Show when={props.artistData?.date}>
<div class={styles.item}>{props.artistData.date}</div> <div class={styles.item}>{props.artistData?.date || ''}</div>
</Show> </Show>
<Show when={props.artistData?.genre}> <Show when={props.artistData?.genre}>
<div class={styles.item}>{props.artistData.genre}</div> <div class={styles.item}>{props.artistData?.genre || ''}</div>
</Show> </Show>
</div> </div>
</Show> </Show>

View File

@ -3,27 +3,32 @@
} }
.playerHeader { .playerHeader {
width: 100%;
display: flex;
justify-content: space-between;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
flex-direction: column; flex-direction: column;
} }
width: 100%;
display: flex;
justify-content: space-between;
} }
.playerTitle { .playerTitle {
@include media-breakpoint-down(sm) {
max-width: 100%;
}
max-width: 50%; max-width: 50%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@include media-breakpoint-down(sm) {
max-width: 100%;
}
} }
.playerControls { .playerControls {
@include media-breakpoint-down(sm) {
margin-top: 20px;
margin-left: 0;
}
display: flex; display: flex;
min-width: 160px; min-width: 160px;
align-items: center; align-items: center;
@ -42,11 +47,6 @@
} }
} }
@include media-breakpoint-down(sm) {
margin-top: 20px;
margin-left: 0;
}
.playButton { .playButton {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,6 +1,6 @@
import { Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '~/types/mediaitem'
import { PlayerHeader } from './PlayerHeader' import { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist' import { PlayerPlaylist } from './PlayerPlaylist'
@ -12,18 +12,22 @@ type Props = {
articleSlug?: string articleSlug?: string
body?: string body?: string
editorMode?: boolean editorMode?: boolean
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void onMediaItemFieldChange?: (
onChangeMediaIndex?: (direction: 'up' | 'down', index) => void index: number,
field: keyof MediaItem | string | number | symbol,
value: string
) => void
onChangeMediaIndex?: (direction: 'up' | 'down', index: number) => void
} }
const getFormattedTime = (point: number) => new Date(point * 1000).toISOString().slice(14, -5) const getFormattedTime = (point: number) => new Date(point * 1000).toISOString().slice(14, -5)
export const AudioPlayer = (props: Props) => { export const AudioPlayer = (props: Props) => {
const audioRef: { current: HTMLAudioElement } = { current: null } let audioRef: HTMLAudioElement | undefined
const gainNodeRef: { current: GainNode } = { current: null } let gainNodeRef: GainNode | undefined
const progressRef: { current: HTMLDivElement } = { current: null } let progressRef: HTMLDivElement | undefined
const audioContextRef: { current: AudioContext } = { current: null } let audioContextRef: AudioContext | undefined
const mouseDownRef: { current: boolean } = { current: false } let mouseDownRef: boolean | undefined
const [currentTrackDuration, setCurrentTrackDuration] = createSignal(0) const [currentTrackDuration, setCurrentTrackDuration] = createSignal(0)
const [currentTime, setCurrentTime] = createSignal(0) const [currentTime, setCurrentTime] = createSignal(0)
@ -31,34 +35,25 @@ export const AudioPlayer = (props: Props) => {
const [isPlaying, setIsPlaying] = createSignal(false) const [isPlaying, setIsPlaying] = createSignal(false)
const currentTack = createMemo(() => props.media[currentTrackIndex()]) const currentTack = createMemo(() => props.media[currentTrackIndex()])
createEffect(on(currentTrackIndex, () => setCurrentTrackDuration(0), { defer: true }))
createEffect(
on(
() => currentTrackIndex(),
() => {
setCurrentTrackDuration(0)
},
{ defer: true }
)
)
const handlePlayMedia = async (trackIndex: number) => { const handlePlayMedia = async (trackIndex: number) => {
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex()) setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
setCurrentTrackIndex(trackIndex) setCurrentTrackIndex(trackIndex)
if (audioContextRef.current.state === 'suspended') { if (audioContextRef?.state === 'suspended') {
await audioContextRef.current.resume() await audioContextRef?.resume()
} }
if (isPlaying()) { if (isPlaying()) {
await audioRef.current.play() await audioRef?.play()
} else { } else {
audioRef.current.pause() audioRef?.pause()
} }
} }
const handleVolumeChange = (volume: number) => { const handleVolumeChange = (volume: number) => {
gainNodeRef.current.gain.value = volume if (gainNodeRef) gainNodeRef.gain.value = volume
} }
const handleAudioEnd = () => { const handleAudioEnd = () => {
@ -67,21 +62,22 @@ export const AudioPlayer = (props: Props) => {
return return
} }
audioRef.current.currentTime = 0 if (audioRef) audioRef.currentTime = 0
setIsPlaying(false) setIsPlaying(false)
setCurrentTrackIndex(0) setCurrentTrackIndex(0)
} }
const handleAudioTimeUpdate = () => { const handleAudioTimeUpdate = () => {
setCurrentTime(audioRef.current.currentTime) setCurrentTime(audioRef?.currentTime || 0)
} }
onMount(() => { onMount(() => {
audioContextRef.current = new AudioContext() audioContextRef = new AudioContext()
gainNodeRef.current = audioContextRef.current.createGain() gainNodeRef = audioContextRef.createGain()
if (audioRef) {
const track = audioContextRef.current.createMediaElementSource(audioRef.current) const track = audioContextRef?.createMediaElementSource(audioRef)
track.connect(gainNodeRef.current).connect(audioContextRef.current.destination) track.connect(gainNodeRef).connect(audioContextRef?.destination)
}
}) })
const playPrevTrack = () => { const playPrevTrack = () => {
@ -102,13 +98,18 @@ export const AudioPlayer = (props: Props) => {
setCurrentTrackIndex(newCurrentTrackIndex) setCurrentTrackIndex(newCurrentTrackIndex)
} }
const handleMediaItemFieldChange = (index: number, field: keyof MediaItem, value) => { const handleMediaItemFieldChange = (
props.onMediaItemFieldChange(index, field, value) index: number,
field: keyof MediaItem | string | number | symbol,
value: string
) => {
props.onMediaItemFieldChange?.(index, field, value)
} }
const scrub = (event) => { const scrub = (event: MouseEvent | undefined) => {
audioRef.current.currentTime = if (progressRef && audioRef) {
(event.offsetX / progressRef.current.offsetWidth) * currentTrackDuration() audioRef.currentTime = (event?.offsetX || 0 / progressRef.offsetWidth) * currentTrackDuration()
}
} }
return ( return (
@ -125,11 +126,11 @@ export const AudioPlayer = (props: Props) => {
<div class={styles.timeline}> <div class={styles.timeline}>
<div <div
class={styles.progress} class={styles.progress}
ref={(el) => (progressRef.current = el)} ref={(el) => (progressRef = el)}
onClick={(e) => scrub(e)} onClick={scrub}
onMouseMove={(e) => mouseDownRef.current && scrub(e)} onMouseMove={(e) => mouseDownRef && scrub(e)}
onMouseDown={() => (mouseDownRef.current = true)} onMouseDown={() => (mouseDownRef = true)}
onMouseUp={() => (mouseDownRef.current = false)} onMouseUp={() => (mouseDownRef = false)}
> >
<div <div
class={styles.progressFilled} class={styles.progressFilled}
@ -145,13 +146,13 @@ export const AudioPlayer = (props: Props) => {
</Show> </Show>
</div> </div>
<audio <audio
ref={(el) => (audioRef.current = el)} ref={(el) => (audioRef = el)}
onTimeUpdate={handleAudioTimeUpdate} onTimeUpdate={handleAudioTimeUpdate}
src={currentTack().url.replace('images.discours.io', 'cdn.discours.io')} src={currentTack().url.replace('images.discours.io', 'cdn.discours.io')}
onCanPlay={() => { onCanPlay={() => {
// start to play the next track on src change // start to play the next track on src change
if (isPlaying()) { if (isPlaying() && audioRef) {
audioRef.current.play() audioRef.play()
} }
}} }}
onLoadedMetadata={({ currentTarget }) => setCurrentTrackDuration(currentTarget.duration)} onLoadedMetadata={({ currentTarget }) => setCurrentTrackDuration(currentTarget.duration)}
@ -162,7 +163,7 @@ export const AudioPlayer = (props: Props) => {
<PlayerPlaylist <PlayerPlaylist
editorMode={props.editorMode} editorMode={props.editorMode}
onPlayMedia={handlePlayMedia} onPlayMedia={handlePlayMedia}
onChangeMediaIndex={(direction, index) => props.onChangeMediaIndex(direction, index)} onChangeMediaIndex={(direction, index) => props.onChangeMediaIndex?.(direction, index)}
isPlaying={isPlaying()} isPlaying={isPlaying()}
media={props.media} media={props.media}
currentTrackIndex={currentTrackIndex()} currentTrackIndex={currentTrackIndex()}

View File

@ -1,10 +1,9 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { Icon } from '~/components/_shared/Icon'
import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '~/types/mediaitem'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss' import styles from './AudioPlayer.module.scss'
type Props = { type Props = {
@ -17,10 +16,7 @@ type Props = {
} }
export const PlayerHeader = (props: Props) => { export const PlayerHeader = (props: Props) => {
const volumeContainerRef: { current: HTMLDivElement } = { let volumeContainerRef: HTMLDivElement | undefined
current: null
}
const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false) const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false)
const toggleVolumeBar = () => { const toggleVolumeBar = () => {
@ -65,7 +61,7 @@ export const PlayerHeader = (props: Props) => {
> >
<Icon name="player-arrow" /> <Icon name="player-arrow" />
</button> </button>
<div ref={(el) => (volumeContainerRef.current = el)} class={styles.volumeContainer}> <div ref={(el) => (volumeContainerRef = el)} class={styles.volumeContainer}>
<Show when={isVolumeBarOpened()}> <Show when={isVolumeBarOpened()}>
<input <input
type="range" type="range"
@ -78,7 +74,7 @@ export const PlayerHeader = (props: Props) => {
onChange={({ target }) => props.onVolumeChange(Number(target.value))} onChange={({ target }) => props.onVolumeChange(Number(target.value))}
/> />
</Show> </Show>
<button onClick={toggleVolumeBar} class={styles.volumeButton} role="button" aria-label="Volume"> <button onClick={toggleVolumeBar} class={styles.volumeButton} aria-label="Volume">
<Icon name="volume" /> <Icon name="volume" />
</button> </button>
</div> </div>

View File

@ -1,17 +1,16 @@
import { gtag } from 'ga-gtag'
import { For, Show, createSignal, lazy } from 'solid-js' import { For, Show, createSignal, lazy } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { Icon } from '~/components/_shared/Icon'
import { MediaItem } from '../../../pages/types' import { Popover } from '~/components/_shared/Popover'
import { getDescription } from '../../../utils/meta' import { useLocalize } from '~/context/localize'
import { Icon } from '../../_shared/Icon' import { MediaItem } from '~/types/mediaitem'
import { Popover } from '../../_shared/Popover' import { descFromBody } from '~/utils/meta'
import { SharePopup, getShareUrl } from '../SharePopup' import { SharePopup, getShareUrl } from '../SharePopup'
import styles from './AudioPlayer.module.scss' import styles from './AudioPlayer.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) const MicroEditor = lazy(() => import('../../Editor/MicroEditor'))
const GrowingTextarea = lazy(() => import('../../_shared/GrowingTextarea/GrowingTextarea')) const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea'))
type Props = { type Props = {
media: MediaItem[] media: MediaItem[]
@ -22,29 +21,30 @@ type Props = {
body?: string body?: string
editorMode?: boolean editorMode?: boolean
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void
onChangeMediaIndex?: (direction: 'up' | 'down', index) => void onChangeMediaIndex?: (direction: 'up' | 'down', index: number) => void
} }
const getMediaTitle = (itm: MediaItem, idx: number) => `${idx}. ${itm.artist} - ${itm.title}` const _getMediaTitle = (itm: MediaItem, idx: number) => `${idx}. ${itm.artist} - ${itm.title}`
export const PlayerPlaylist = (props: Props) => { export const PlayerPlaylist = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const [activeEditIndex, setActiveEditIndex] = createSignal(-1) const [activeEditIndex, setActiveEditIndex] = createSignal(-1)
const toggleDropDown = (index) => { const toggleDropDown = (index: number) => {
setActiveEditIndex(activeEditIndex() === index ? -1 : index) setActiveEditIndex(activeEditIndex() === index ? -1 : index)
} }
const handleMediaItemFieldChange = (field: keyof MediaItem, value: string) => { const handleMediaItemFieldChange = (field: keyof MediaItem, value: string) => {
props.onMediaItemFieldChange(activeEditIndex(), field, value) props.onMediaItemFieldChange?.(activeEditIndex(), field, value)
} }
const play = (index: number) => { const play = (index: number) => {
const mi = props.media[index] props.onPlayMedia(index)
gtag('event', 'select_item', { //const mi = props.media[index]
item_list_id: props.articleSlug, //gtag('event', 'select_item', {
item_list_name: getMediaTitle(mi, index), //item_list_id: props.articleSlug,
items: props.media.map((it, ix) => getMediaTitle(it, ix)) //item_list_name: getMediaTitle(mi, index),
}) //items: props.media.map((it, ix) => getMediaTitle(it, ix)),
//})
} }
return ( return (
<ul class={styles.playlist}> <ul class={styles.playlist}>
@ -89,26 +89,26 @@ export const PlayerPlaylist = (props: Props) => {
<div class={styles.actions}> <div class={styles.actions}>
<Show when={props.editorMode}> <Show when={props.editorMode}>
<Popover content={t('Move up')}> <Popover content={t('Move up')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<button <button
type="button" type="button"
ref={triggerRef} ref={triggerRef}
class={styles.action} class={styles.action}
disabled={index() === 0} disabled={index() === 0}
onClick={() => props.onChangeMediaIndex('up', index())} onClick={() => props.onChangeMediaIndex?.('up', index())}
> >
<Icon name="up-button" /> <Icon name="up-button" />
</button> </button>
)} )}
</Popover> </Popover>
<Popover content={t('Move down')}> <Popover content={t('Move down')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<button <button
type="button" type="button"
ref={triggerRef} ref={triggerRef}
class={styles.action} class={styles.action}
disabled={index() === props.media.length - 1} disabled={index() === props.media.length - 1}
onClick={() => props.onChangeMediaIndex('down', index())} onClick={() => props.onChangeMediaIndex?.('down', index())}
> >
<Icon name="up-button" class={styles.moveIconDown} /> <Icon name="up-button" class={styles.moveIconDown} />
</button> </button>
@ -117,7 +117,7 @@ export const PlayerPlaylist = (props: Props) => {
</Show> </Show>
<Show when={(mi.lyrics || mi.body) && !props.editorMode}> <Show when={(mi.lyrics || mi.body) && !props.editorMode}>
<Popover content={t('Show lyrics')}> <Popover content={t('Show lyrics')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<button ref={triggerRef} type="button" onClick={() => toggleDropDown(index())}> <button ref={triggerRef} type="button" onClick={() => toggleDropDown(index())}>
<Icon name="list" /> <Icon name="list" />
</button> </button>
@ -125,7 +125,7 @@ export const PlayerPlaylist = (props: Props) => {
</Popover> </Popover>
</Show> </Show>
<Popover content={props.editorMode ? t('Edit') : t('Share')}> <Popover content={props.editorMode ? t('Edit') : t('Share')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<div ref={triggerRef}> <div ref={triggerRef}>
<Show <Show
when={!props.editorMode} when={!props.editorMode}
@ -137,8 +137,8 @@ export const PlayerPlaylist = (props: Props) => {
> >
<SharePopup <SharePopup
title={mi.title} title={mi.title}
description={getDescription(props.body)} description={descFromBody(props.body || '')}
imageUrl={mi.pic} imageUrl={mi.pic || ''}
shareUrl={getShareUrl({ pathname: `/${props.articleSlug}` })} shareUrl={getShareUrl({ pathname: `/${props.articleSlug}` })}
trigger={ trigger={
<div> <div>
@ -171,11 +171,10 @@ export const PlayerPlaylist = (props: Props) => {
} }
> >
<div class={styles.descriptionBlock}> <div class={styles.descriptionBlock}>
<SimplifiedEditor <MicroEditor
initialContent={mi.body} content={mi.body}
placeholder={`${t('Description')}...`} placeholder={`${t('Description')}...`}
smallHeight={true} onChange={(value: string) => handleMediaItemFieldChange('body', value)}
onChange={(value) => handleMediaItemFieldChange('body', value)}
/> />
<GrowingTextarea <GrowingTextarea
allowEnterKey={true} allowEnterKey={true}

View File

@ -1,14 +1,14 @@
.comment { .comment {
@include media-breakpoint-down(sm) {
padding-right: 0;
}
margin: 0 0 0.5em; margin: 0 0 0.5em;
padding: 0 1rem; padding: 0 1rem;
transition: background-color 0.3s; transition: background-color 0.3s;
position: relative; position: relative;
list-style: none; list-style: none;
@include media-breakpoint-down(sm) {
padding-right: 0;
}
&.isNew { &.isNew {
border-radius: 6px; border-radius: 6px;
background: rgb(38 56 217 / 5%); background: rgb(38 56 217 / 5%);
@ -179,6 +179,10 @@
@include font-size(1.2rem); @include font-size(1.2rem);
} }
.commentAuthor {
margin-right: 2rem;
}
.articleAuthor { .articleAuthor {
@include font-size(1.2rem); @include font-size(1.2rem);
@ -189,9 +193,6 @@
.articleLink { .articleLink {
@include font-size(1.2rem); @include font-size(1.2rem);
flex: 0 0 50%;
margin-right: 2em;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
margin: 0.3em 0 0.5em; margin: 0.3em 0 0.5em;
} }
@ -204,20 +205,25 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
flex: 0 0 50%;
margin-right: 2em;
} }
.articleLinkIcon { .articleLinkIcon {
@include media-breakpoint-up(md) {
margin-left: 1em;
}
display: inline-block; display: inline-block;
margin-right: 1em; margin-right: 1em;
vertical-align: middle; vertical-align: middle;
width: 1em; width: 1em;
@include media-breakpoint-up(md) {
margin-left: 1em;
}
} }
.commentDates { .commentDates {
@include font-size(1.2rem);
flex: 1; flex: 1;
display: flex; display: flex;
gap: 1rem; gap: 1rem;
@ -227,8 +233,6 @@
margin: 0 1em 4px 0; margin: 0 1em 4px 0;
color: rgb(0 0 0 / 30%); color: rgb(0 0 0 / 30%);
@include font-size(1.2rem);
.date { .date {
.icon { .icon {
line-height: 1; line-height: 1;
@ -242,13 +246,13 @@
} }
.commentDetails { .commentDetails {
padding: 1rem 0.2rem 0;
margin-bottom: 1.2rem;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
align-items: center; align-items: center;
display: flex; display: flex;
} }
padding: 1rem 0.2rem 0;
margin-bottom: 1.2rem;
} }
.compactUserpic { .compactUserpic {

View File

@ -1,24 +1,27 @@
import { getPagePath } from '@nanostores/router' import { A } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js' import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js'
import { Icon } from '~/components/_shared/Icon'
import { useConfirm } from '../../../context/confirm' import { ShowIfAuthenticated } from '~/components/_shared/ShowIfAuthenticated'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '../../../context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '../../../context/session' import { useSession } from '~/context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar, useUI } from '~/context/ui'
import { Author, Reaction, ReactionKind } from '../../../graphql/schema/core.gen' import deleteReactionMutation from '~/graphql/mutation/core/reaction-destroy'
import { router } from '../../../stores/router' import {
Author,
MutationCreate_ReactionArgs,
MutationUpdate_ReactionArgs,
Reaction,
ReactionKind
} from '~/graphql/schema/core.gen'
import { AuthorLink } from '../../Author/AuthorLink' import { AuthorLink } from '../../Author/AuthorLink'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { CommentDate } from '../CommentDate' import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl' import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss' import styles from './Comment.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) const MiniEditor = lazy(() => import('../../Editor/MiniEditor'))
type Props = { type Props = {
comment: Reaction comment: Reaction
@ -30,6 +33,7 @@ type Props = {
showArticleLink?: boolean showArticleLink?: boolean
clickedReply?: (id: number) => void clickedReply?: (id: number) => void
clickedReplyId?: number clickedReplyId?: number
onDelete?: (id: number) => void
} }
export const Comment = (props: Props) => { export const Comment = (props: Props) => {
@ -37,23 +41,22 @@ export const Comment = (props: Props) => {
const [isReplyVisible, setIsReplyVisible] = createSignal(false) const [isReplyVisible, setIsReplyVisible] = createSignal(false)
const [loading, setLoading] = createSignal(false) const [loading, setLoading] = createSignal(false)
const [editMode, setEditMode] = createSignal(false) const [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false) const [editedBody, setEditedBody] = createSignal<string>()
const { author, session } = useSession() const { session, client } = useSession()
const { createReaction, deleteReaction, updateReaction } = useReactions() const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { showConfirm } = useConfirm() const { createShoutReaction, updateShoutReaction } = useReactions()
const { showConfirm } = useUI()
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const canEdit = createMemo( const canEdit = createMemo(
() => () =>
Boolean(author()?.id) && Boolean(author()?.id) &&
(props.comment?.created_by?.id === author().id || session()?.user?.roles.includes('editor')) (props.comment?.created_by?.slug === author()?.slug || session()?.user?.roles?.includes('editor'))
) )
const comment = createMemo(() => props.comment) const body = createMemo(() => (editedBody() ? editedBody()?.trim() : props.comment.body?.trim() || ''))
const body = createMemo(() => (comment().body || '').trim())
const remove = async () => { const remove = async () => {
if (comment()?.id) { if (props.comment?.id) {
try { try {
const isConfirmed = await showConfirm({ const isConfirmed = await showConfirm({
confirmBody: t('Are you sure you want to delete this comment?'), confirmBody: t('Are you sure you want to delete this comment?'),
@ -63,47 +66,68 @@ export const Comment = (props: Props) => {
}) })
if (isConfirmed) { if (isConfirmed) {
await deleteReaction(comment().id) const resp = await client()
?.mutation(deleteReactionMutation, { id: props.comment.id })
.toPromise()
const result = resp?.data?.delete_reaction
const { error } = result
const notificationType = error ? 'error' : 'success'
const notificationMessage = error
? t('Failed to delete comment')
: t('Comment successfully deleted')
await showSnackbar({
type: notificationType,
body: notificationMessage,
duration: 3
})
await showSnackbar({ body: t('Comment successfully deleted') }) if (!error && props.onDelete) {
props.onDelete(props.comment.id)
}
} }
} catch (error) { } catch (error) {
await showSnackbar({ body: 'error' })
console.error('[deleteReaction]', error) console.error('[deleteReaction]', error)
} }
} }
} }
const handleCreate = async (value) => { const handleCreate = async (value: string) => {
try { try {
setLoading(true) setLoading(true)
await createReaction({ await createShoutReaction({
reaction: {
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
reply_to: props.comment.id, reply_to: props.comment.id,
body: value, body: value,
shout: props.comment.shout.id shout: props.comment.shout.id
}) }
setClearEditor(true) } as MutationCreate_ReactionArgs)
setIsReplyVisible(false) setIsReplyVisible(false)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
console.error('[handleCreate reaction]:', error) console.error('[handleCreate reaction]:', error)
} }
setClearEditor(false)
} }
const toggleEditMode = () => { const toggleEditMode = () => {
setEditMode((oldEditMode) => !oldEditMode) setEditMode((oldEditMode) => !oldEditMode)
} }
const handleUpdate = async (value) => { const handleUpdate = async (value: string) => {
setLoading(true) setLoading(true)
try { try {
await updateReaction({ const reaction = await updateShoutReaction({
id: props.comment.id, reaction: {
id: props.comment.id || 0,
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
body: value, body: value,
shout: props.comment.shout.id shout: props.comment.shout.id
}) }
} as MutationUpdate_ReactionArgs)
if (reaction) {
setEditedBody(value)
}
setEditMode(false) setEditMode(false)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
@ -113,8 +137,11 @@ export const Comment = (props: Props) => {
return ( return (
<li <li
id={`comment_${comment().id}`} id={`comment_${props.comment.id}`}
class={clsx(styles.comment, props.class, { [styles.isNew]: comment()?.created_at > props.lastSeen })} class={clsx(styles.comment, props.class, {
[styles.isNew]:
(props.lastSeen || Date.now()) > (props.comment.updated_at || props.comment.created_at)
})}
> >
<Show when={!!body()}> <Show when={!!body()}>
<div class={styles.commentContent}> <div class={styles.commentContent}>
@ -123,21 +150,21 @@ export const Comment = (props: Props) => {
fallback={ fallback={
<div> <div>
<Userpic <Userpic
name={comment().created_by.name} name={props.comment.created_by.name || ''}
userpic={comment().created_by.pic} userpic={props.comment.created_by.pic || ''}
class={clsx({ class={clsx({
[styles.compactUserpic]: props.compact [styles.compactUserpic]: props.compact
})} })}
/> />
<small> <small>
<a href={`#comment_${comment()?.id}`}>{comment()?.shout.title || ''}</a> <a href={`#comment_${props.comment?.id}`}>{props.comment?.shout.title || ''}</a>
</small> </small>
</div> </div>
} }
> >
<div class={styles.commentDetails}> <div class={styles.commentDetails}>
<div class={styles.commentAuthor}> <div class={styles.commentAuthor}>
<AuthorLink author={comment()?.created_by as Author} /> <AuthorLink author={props.comment?.created_by as Author} />
</div> </div>
<Show when={props.isArticleAuthor}> <Show when={props.isArticleAuthor}>
@ -147,32 +174,23 @@ export const Comment = (props: Props) => {
<Show when={props.showArticleLink}> <Show when={props.showArticleLink}>
<div class={styles.articleLink}> <div class={styles.articleLink}>
<Icon name="arrow-right" class={styles.articleLinkIcon} /> <Icon name="arrow-right" class={styles.articleLinkIcon} />
<a <A href={`${props.comment.shout.slug}?commentId=${props.comment.id}`}>
href={`${getPagePath(router, 'article', { slug: comment().shout.slug })}?commentId=${ {props.comment.shout.title}
comment().id </A>
}`}
>
{comment().shout.title}
</a>
</div> </div>
</Show> </Show>
<CommentDate showOnHover={true} comment={comment()} isShort={true} /> <CommentDate showOnHover={true} comment={props.comment} isShort={true} />
<CommentRatingControl comment={comment()} /> <CommentRatingControl comment={props.comment} />
</div> </div>
</Show> </Show>
<div class={styles.commentBody}> <div class={styles.commentBody}>
<Show when={editMode()} fallback={<div innerHTML={body()} />}> <Show when={editMode()} fallback={<div innerHTML={body()} />}>
<Suspense fallback={<p>{t('Loading')}</p>}> <Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor <MiniEditor
initialContent={comment().body} content={editedBody() || props.comment.body || ''}
submitButtonText={t('Save')}
quoteEnabled={true}
imageEnabled={true}
placeholder={t('Write a comment...')} placeholder={t('Write a comment...')}
onSubmit={(value) => handleUpdate(value)} onSubmit={(value) => handleUpdate(value)}
submitByCtrlEnter={true}
onCancel={() => setEditMode(false)} onCancel={() => setEditMode(false)}
setClear={clearEditor()}
/> />
</Suspense> </Suspense>
</Show> </Show>
@ -185,7 +203,7 @@ export const Comment = (props: Props) => {
disabled={loading()} disabled={loading()}
onClick={() => { onClick={() => {
setIsReplyVisible(!isReplyVisible()) setIsReplyVisible(!isReplyVisible())
props.clickedReply(props.comment.id) props.clickedReply?.(props.comment.id)
}} }}
class={clsx(styles.commentControl, styles.commentControlReply)} class={clsx(styles.commentControl, styles.commentControlReply)}
> >
@ -226,18 +244,15 @@ export const Comment = (props: Props) => {
{/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/} {/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/}
{/* onClick={() => showModal('reportComment')}*/} {/* onClick={() => showModal('reportComment')}*/}
{/*>*/} {/*>*/}
{/* {t('Report')}*/} {/* {t('Complain')}*/}
{/*</button>*/} {/*</button>*/}
</div> </div>
<Show when={isReplyVisible() && props.clickedReplyId === props.comment.id}> <Show when={isReplyVisible() && props.clickedReplyId === props.comment.id}>
<Suspense fallback={<p>{t('Loading')}</p>}> <Suspense fallback={<p>{t('Loading')}</p>}>
<SimplifiedEditor <MiniEditor
quoteEnabled={true}
imageEnabled={true}
placeholder={t('Write a comment...')} placeholder={t('Write a comment...')}
onSubmit={(value) => handleCreate(value)} onSubmit={(value) => handleCreate(value)}
submitByCtrlEnter={true}
/> />
</Suspense> </Suspense>
</Show> </Show>
@ -246,7 +261,7 @@ export const Comment = (props: Props) => {
</Show> </Show>
<Show when={props.sortedComments}> <Show when={props.sortedComments}>
<ul> <ul>
<For each={props.sortedComments.filter((r) => r.reply_to === props.comment.id)}> <For each={props.sortedComments?.filter((r) => r.reply_to === props.comment.id)}>
{(c) => ( {(c) => (
<Comment <Comment
sortedComments={props.sortedComments} sortedComments={props.sortedComments}

View File

@ -2,29 +2,17 @@
@include font-size(1.2rem); @include font-size(1.2rem);
color: var(--secondary-color); color: var(--secondary-color);
align-items: center;
align-self: center;
display: flex; display: flex;
justify-content: center;
flex-direction: column;
flex: 1; flex: 1;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.2rem; font-size: 1.2rem;
justify-content: flex-start;
margin: 0 1rem;
height: 1.6rem;
.date { .date {
font-weight: 500; font-weight: 500;
margin-right: 1rem; margin-right: 1rem;
position: relative; position: relative;
.icon {
line-height: 1;
width: 1rem;
display: inline-block;
opacity: 0.6;
margin-right: 0.5rem;
vertical-align: middle;
}
} }
&.showOnHover { &.showOnHover {

View File

@ -1,10 +1,8 @@
import type { Reaction } from '../../../graphql/schema/core.gen' import type { Reaction } from '~/graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { Icon } from '../../_shared/Icon'
import styles from './CommentDate.module.scss' import styles from './CommentDate.module.scss'
@ -16,7 +14,7 @@ type Props = {
} }
export const CommentDate = (props: Props) => { export const CommentDate = (props: Props) => {
const { t, formatDate } = useLocalize() const { formatDate } = useLocalize()
const formattedDate = (date: number) => { const formattedDate = (date: number) => {
const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort
@ -34,14 +32,6 @@ export const CommentDate = (props: Props) => {
})} })}
> >
<time class={styles.date}>{formattedDate(props.comment.created_at * 1000)}</time> <time class={styles.date}>{formattedDate(props.comment.created_at * 1000)}</time>
<Show when={props.comment.updated_at}>
<time class={styles.date}>
<Icon name="edit" class={styles.icon} />
<span class={styles.text}>
{t('Edited')} {formattedDate(props.comment.updated_at * 1000)}
</span>
</time>
</Show>
</div> </div>
) )
} }

View File

@ -1,12 +1,12 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useFeed } from '~/context/feed'
import { useReactions } from '../../context/reactions' import { useLocalize } from '~/context/localize'
import { useSession } from '../../context/session' import { useReactions } from '~/context/reactions'
import { useSnackbar } from '../../context/snackbar' import { useSession } from '~/context/session'
import { Reaction, ReactionKind } from '../../graphql/schema/core.gen' import { useSnackbar } from '~/context/ui'
import { loadShout } from '../../stores/zine/articles' import { Reaction, ReactionKind } from '~/graphql/schema/core.gen'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList' import { VotersList } from '../_shared/VotersList'
@ -18,21 +18,23 @@ type Props = {
export const CommentRatingControl = (props: Props) => { export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { author } = useSession() const { loadShout } = useFeed()
const { session } = useSession()
const uid = createMemo<number>(() => session()?.user?.app_data?.profile?.id || 0)
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() const { reactionEntities, createShoutReaction, deleteShoutReaction, loadReactionsBy } = useReactions()
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some( Object.values(reactionEntities).some(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.created_by.slug === author()?.slug && r.created_by.id === uid() &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id r.reply_to === props.comment.id
) )
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like)) const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike)) const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const canVote = createMemo(() => author()?.slug !== props.comment.created_by.slug) const canVote = createMemo(() => uid() !== props.comment.created_by.id)
const commentRatingReactions = createMemo(() => const commentRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter( Object.values(reactionEntities).filter(
@ -47,11 +49,11 @@ export const CommentRatingControl = (props: Props) => {
const reactionToDelete = Object.values(reactionEntities).find( const reactionToDelete = Object.values(reactionEntities).find(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.created_by.slug === author()?.slug && r.created_by.id === uid() &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id r.reply_to === props.comment.id
) )
return deleteReaction(reactionToDelete.id) if (reactionToDelete) return deleteShoutReaction(reactionToDelete.id)
} }
const handleRatingChange = async (isUpvote: boolean) => { const handleRatingChange = async (isUpvote: boolean) => {
@ -61,10 +63,12 @@ export const CommentRatingControl = (props: Props) => {
} else if (isDownvoted()) { } else if (isDownvoted()) {
await deleteCommentReaction(ReactionKind.Dislike) await deleteCommentReaction(ReactionKind.Dislike)
} else { } else {
await createReaction({ await createShoutReaction({
reaction: {
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id, shout: props.comment.shout.id,
reply_to: props.comment.id reply_to: props.comment.id
}
}) })
} }
} catch { } catch {
@ -80,8 +84,7 @@ export const CommentRatingControl = (props: Props) => {
return ( return (
<div class={styles.commentRating}> <div class={styles.commentRating}>
<button <button
role="button" disabled={!(canVote() && uid())}
disabled={!(canVote() && author())}
onClick={() => handleRatingChange(true)} onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, { class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
[styles.voted]: isUpvoted() [styles.voted]: isUpvoted()
@ -91,11 +94,11 @@ export const CommentRatingControl = (props: Props) => {
trigger={ trigger={
<div <div
class={clsx(styles.commentRatingValue, { class={clsx(styles.commentRatingValue, {
[styles.commentRatingPositive]: props.comment.stat.rating > 0, [styles.commentRatingPositive]: (props.comment?.stat?.rating || 0) > 0,
[styles.commentRatingNegative]: props.comment.stat.rating < 0 [styles.commentRatingNegative]: (props.comment?.stat?.rating || 0) < 0
})} })}
> >
{props.comment.stat.rating || 0} {props.comment?.stat?.rating || 0}
</div> </div>
} }
variant="tiny" variant="tiny"
@ -106,8 +109,7 @@ export const CommentRatingControl = (props: Props) => {
/> />
</Popup> </Popup>
<button <button
role="button" disabled={!(canVote() && uid())}
disabled={!(canVote() && author())}
onClick={() => handleRatingChange(false)} onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, { class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted() [styles.voted]: isDownvoted()

View File

@ -1,40 +1,20 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createMemo, createSignal, lazy, onMount } from 'solid-js' import { For, Show, createMemo, createSignal, lazy, onMount } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useFeed } from '~/context/feed'
import { useReactions } from '../../context/reactions' import { useLocalize } from '~/context/localize'
import { useSession } from '../../context/session' import { useReactions } from '~/context/reactions'
import { Author, Reaction, ReactionKind } from '../../graphql/schema/core.gen' import { useSession } from '~/context/session'
import { byCreated } from '../../utils/sortby' import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/core.gen'
import { SortFunction } from '~/types/common'
import { byCreated, byStat } from '~/utils/sort'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Loading } from '../_shared/Loading'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import styles from './Article.module.scss'
import { Comment } from './Comment' import { Comment } from './Comment'
import styles from './Article.module.scss' const MiniEditor = lazy(() => import('../Editor/MiniEditor'))
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
if (a.reply_to && b.reply_to) {
return 0
}
const x = a.stat?.rating || 0
const y = b.stat?.rating || 0
if (x > y) {
return 1
}
if (x < y) {
return -1
}
return 0
}
type Props = { type Props = {
articleAuthors: Author[] articleAuthors: Author[]
@ -43,63 +23,70 @@ type Props = {
} }
export const CommentsTree = (props: Props) => { export const CommentsTree = (props: Props) => {
const { author } = useSession() const { session } = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const [commentsOrder, setCommentsOrder] = createSignal<CommentsOrder>('createdAt') const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest)
const [onlyNew, setOnlyNew] = createSignal(false)
const [newReactions, setNewReactions] = createSignal<Reaction[]>([]) const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>() const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const { reactionEntities, createReaction } = useReactions() const { reactionEntities, createShoutReaction, loadReactionsBy } = useReactions()
const comments = createMemo(() => const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') Object.values(reactionEntities()).filter((reaction) => reaction.kind === 'COMMENT')
) )
const sortedComments = createMemo(() => { const sortedComments = createMemo(() => {
let newSortedComments = [...comments()] let newSortedComments = [...comments()]
newSortedComments = newSortedComments.sort(byCreated) newSortedComments = newSortedComments.sort(byCreated)
if (commentsOrder() === 'newOnly') { if (onlyNew()) {
return newReactions().reverse() return newReactions().sort(byCreated).reverse()
} }
if (commentsOrder() === 'rating') { if (commentsOrder() === ReactionSort.Like) {
newSortedComments = newSortedComments.sort(sortCommentsByRating) newSortedComments = newSortedComments.sort(byStat('rating') as SortFunction<Reaction>)
} }
return newSortedComments return newSortedComments
}) })
const { seen } = useFeed()
const dateFromLocalStorage = Number.parseInt(localStorage.getItem(`${props.shoutSlug}`)) const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0)
const currentDate = new Date()
const setCookie = () => localStorage.setItem(`${props.shoutSlug}`, `${currentDate}`)
onMount(() => { onMount(() => {
if (!dateFromLocalStorage) { const currentDate = new Date()
const setCookie = () => localStorage?.setItem(`${props.shoutSlug}`, `${currentDate}`)
if (!shoutLastSeen()) {
setCookie() setCookie()
} else if (currentDate.getTime() > dateFromLocalStorage) { } else if (currentDate.getTime() > shoutLastSeen()) {
const newComments = comments().filter((c) => { const newComments = comments().filter((c) => {
if (c.reply_to || c.created_by.slug === author()?.slug) { if (
(session()?.user?.app_data?.profile?.id && c.reply_to) ||
c.created_by.id === session()?.user?.app_data?.profile?.id
) {
return return
} }
const created = c.created_at return (c.updated_at || c.created_at) > shoutLastSeen()
return created > dateFromLocalStorage
}) })
setNewReactions(newComments) setNewReactions(newComments)
setCookie() setCookie()
} }
}) })
const handleSubmitComment = async (value) => {
const [posting, setPosting] = createSignal(false)
const handleSubmitComment = async (value: string) => {
setPosting(true)
try { try {
await createReaction({ await createShoutReaction({
reaction: {
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
body: value, body: value,
shout: props.shoutId shout: props.shoutId
}
}) })
setClearEditor(true) await loadReactionsBy({ by: { shout: props.shoutSlug } })
} catch (error) { } catch (error) {
console.error('[handleCreate reaction]:', error) console.error('[handleCreate reaction]:', error)
} }
setClearEditor(false) setPosting(false)
} }
return ( return (
@ -108,37 +95,31 @@ export const CommentsTree = (props: Props) => {
<h2 class={styles.commentsHeader}> <h2 class={styles.commentsHeader}>
{t('Comments')} {comments().length.toString() || ''} {t('Comments')} {comments().length.toString() || ''}
<Show when={newReactions().length > 0}> <Show when={newReactions().length > 0}>
<span class={styles.newReactions}>&nbsp;+{newReactions().length}</span> <span class={styles.newReactions}>{` +${newReactions().length}`}</span>
</Show> </Show>
</h2> </h2>
<Show when={comments().length > 0}> <Show when={comments().length > 0}>
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}> <ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
<Show when={newReactions().length > 0}> <Show when={newReactions().length > 0}>
<li classList={{ 'view-switcher__item--selected': commentsOrder() === 'newOnly' }}> <li classList={{ 'view-switcher__item--selected': onlyNew() }}>
<Button <Button variant="light" value={t('New only')} onClick={() => setOnlyNew(!onlyNew())} />
variant="light"
value={t('New only')}
onClick={() => {
setCommentsOrder('newOnly')
}}
/>
</li> </li>
</Show> </Show>
<li classList={{ 'view-switcher__item--selected': commentsOrder() === 'createdAt' }}> <li classList={{ 'view-switcher__item--selected': commentsOrder() === ReactionSort.Newest }}>
<Button <Button
variant="light" variant="light"
value={t('By time')} value={t('By time')}
onClick={() => { onClick={() => {
setCommentsOrder('createdAt') setCommentsOrder(ReactionSort.Newest)
}} }}
/> />
</li> </li>
<li classList={{ 'view-switcher__item--selected': commentsOrder() === 'rating' }}> <li classList={{ 'view-switcher__item--selected': commentsOrder() === ReactionSort.Like }}>
<Button <Button
variant="light" variant="light"
value={t('By rating')} value={t('By rating')}
onClick={() => { onClick={() => {
setCommentsOrder('rating') setCommentsOrder(ReactionSort.Like)
}} }}
/> />
</li> </li>
@ -150,13 +131,11 @@ export const CommentsTree = (props: Props) => {
{(reaction) => ( {(reaction) => (
<Comment <Comment
sortedComments={sortedComments()} sortedComments={sortedComments()}
isArticleAuthor={Boolean( isArticleAuthor={Boolean(props.articleAuthors.some((a) => a?.id === reaction.created_by.id))}
props.articleAuthors.some((a) => a?.slug === reaction.created_by.slug)
)}
comment={reaction} comment={reaction}
clickedReply={(id) => setClickedReplyId(id)} clickedReply={(id) => setClickedReplyId(id)}
clickedReplyId={clickedReplyId()} clickedReplyId={clickedReplyId()}
lastSeen={dateFromLocalStorage} lastSeen={shoutLastSeen()}
/> />
)} )}
</For> </For>
@ -168,22 +147,17 @@ export const CommentsTree = (props: Props) => {
<a href="?m=auth&mode=register" class={styles.link}> <a href="?m=auth&mode=register" class={styles.link}>
{t('sign up')} {t('sign up')}
</a>{' '} </a>{' '}
{t('or')}&nbsp; {t('or')}{' '}
<a href="?m=auth&mode=login" class={styles.link}> <a href="?m=auth&mode=login" class={styles.link}>
{t('sign in')} {t('sign in')}
</a> </a>
</div> </div>
} }
> >
<SimplifiedEditor <MiniEditor placeholder={t('Write a comment...')} onSubmit={handleSubmitComment} />
quoteEnabled={true} <Show when={posting()}>
imageEnabled={true} <Loading />
autoFocus={false} </Show>
submitByCtrlEnter={true}
placeholder={t('Write a comment...')}
onSubmit={(value) => handleSubmitComment(value)}
setClear={clearEditor()}
/>
</ShowIfAuthenticated> </ShowIfAuthenticated>
</> </>
) )

View File

@ -1,49 +1,44 @@
import type { Author, Shout, Topic } from '../../graphql/schema/core.gen' import { AuthToken } from '@authorizerdev/authorizer-js'
import { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core' import { createPopper } from '@popperjs/core'
import { Link, Meta } from '@solidjs/meta' import { Link } from '@solidjs/meta'
import { A, useSearchParams } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { install } from 'ga-gtag'
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { useFeed } from '~/context/feed'
import { useLocalize } from '../../context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '../../context/session' import { useSession } from '~/context/session'
import { MediaItem } from '../../pages/types' import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
import { showModal } from '../../stores/ui' import { processPrepositions } from '~/intl/prepositions'
import { capitalize } from '../../utils/capitalize' import { isCyrillic } from '~/intl/translate'
import { getImageUrl, getOpenGraphImageUrl } from '../../utils/getImageUrl' import { getImageUrl } from '~/lib/getThumbUrl'
import { getDescription, getKeywords } from '../../utils/meta' import { MediaItem } from '~/types/mediaitem'
import { isCyrillic } from '../../utils/translate' import { capitalize } from '~/utils/capitalize'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic' import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup' import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { Modal } from '../Nav/Modal' import stylesHeader from '../HeaderNav/Header.module.scss'
import { TableOfContents } from '../TableOfContents'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image' import { Image } from '../_shared/Image'
import { InviteMembers } from '../_shared/InviteMembers' import { InviteMembers } from '../_shared/InviteMembers'
import { Lightbox } from '../_shared/Lightbox' import { Lightbox } from '../_shared/Lightbox'
import { Modal } from '../_shared/Modal'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { ShareModal } from '../_shared/ShareModal' import { ShareModal } from '../_shared/ShareModal'
import { ImageSwiper } from '../_shared/SolidSwiper' import { ImageSwiper } from '../_shared/SolidSwiper'
import { TableOfContents } from '../_shared/TableOfContents'
import { VideoPlayer } from '../_shared/VideoPlayer' import { VideoPlayer } from '../_shared/VideoPlayer'
import styles from './Article.module.scss'
import { AudioHeader } from './AudioHeader' import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree' import { CommentsTree } from './CommentsTree'
import { SharePopup, getShareUrl } from './SharePopup' import { SharePopup, getShareUrl } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl' import { ShoutRatingControl } from './ShoutRatingControl'
import stylesHeader from '../Nav/Header/Header.module.scss'
import styles from './Article.module.scss'
type Props = { type Props = {
article: Shout article: Shout
scrollToComments?: boolean
} }
type IframeSize = { type IframeSize = {
@ -52,71 +47,105 @@ type IframeSize = {
} }
export type ArticlePageSearchParams = { export type ArticlePageSearchParams = {
scrollTo: 'comments' commentId?: string
commentId: string slide?: string
} }
const scrollTo = (el: HTMLElement) => { const scrollTo = (el: HTMLElement) => {
const { top } = el.getBoundingClientRect() const { top } = el.getBoundingClientRect()
window.scrollTo({
top: top - DEFAULT_HEADER_OFFSET, window?.scrollTo({
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
left: 0, left: 0,
behavior: 'smooth' behavior: 'smooth'
}) })
} }
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
export const COMMENTS_PER_PAGE = 30
const VOTES_PER_PAGE = 50
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>() const [searchParams] = useSearchParams<ArticlePageSearchParams>()
const { showModal } = useUI()
const { loadReactionsBy } = useReactions() const { loadReactionsBy } = useReactions()
const [selectedImage, setSelectedImage] = createSignal('') const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const { author, session, isAuthenticated, requireAuthentication } = useSession() const { session, requireAuthentication } = useSession()
const { addSeen } = useFeed()
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000))) const [pages, setPages] = createSignal<Record<string, number>>({})
createEffect(
on(
pages,
(p: Record<string, number>) => {
console.debug('content paginated')
loadReactionsBy({
by: { shout: props.article.slug, comment: true },
limit: COMMENTS_PER_PAGE,
offset: COMMENTS_PER_PAGE * p.comments || 0
})
loadReactionsBy({
by: { shout: props.article.slug, rating: true },
limit: VOTES_PER_PAGE,
offset: VOTES_PER_PAGE * p.rating || 0
})
setIsReactionsLoaded(true)
console.debug('reactions paginated')
},
{ defer: true }
)
)
const canEdit = createMemo( const [canEdit, setCanEdit] = createSignal<boolean>(false)
() => createEffect(
Boolean(author()?.id) && on(
(props.article?.authors?.some((a) => Boolean(a) && a?.id === author().id) || () => session(),
props.article?.created_by?.id === author().id || (s?: AuthToken) => {
session()?.user?.roles.includes('editor')) const profile = s?.user?.app_data?.profile
if (!profile) return
const isEditor = s?.user?.roles?.includes('editor')
const isCreator = props.article.created_by?.id === profile.id
const fit = (a: Maybe<Author>) => a?.id === profile.id || isCreator || isEditor
setCanEdit((_: boolean) => Boolean(props.article.authors?.some(fit)))
}
)
) )
const mainTopic = createMemo(() => { const mainTopic = createMemo(() => {
const mainTopicSlug = props.article.topics.length > 0 ? props.article.main_topic : null const mainTopicSlug = (props.article.topics?.length || 0) > 0 ? props.article.main_topic : null
const mt = props.article.topics.find((tpc: Topic) => tpc.slug === mainTopicSlug) const mt = props.article.topics?.find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
if (mt) { if (mt) {
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title mt.title = lang() === 'en' ? capitalize(mt.slug.replaceAll('-', ' ')) : mt.title
return mt return mt
} }
return props.article.topics[0] return props.article.topics?.[0]
}) })
const handleBookmarkButtonClick = (ev) => { const handleBookmarkButtonClick = (ev: MouseEvent | undefined) => {
requireAuthentication(() => { requireAuthentication(() => {
// TODO: implement bookmark clicked // TODO: implement bookmark clicked
ev.preventDefault() ev?.preventDefault()
}, 'bookmark') }, 'bookmark')
} }
const body = createMemo(() => { const body = createMemo(() => {
if (props.article.layout === 'literature') { if (props.article.layout === 'literature') {
try { try {
if (props.article?.media) { if (props.article.media) {
const media = JSON.parse(props.article.media) const media = JSON.parse(props.article.media)
if (media.length > 0) { if (media.length > 0) {
return media[0].body return processPrepositions(media[0].body)
} }
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} }
return props.article.body return processPrepositions(props.article.body) || ''
}) })
const imageUrls = createMemo(() => { const imageUrls = createMemo(() => {
@ -126,10 +155,11 @@ export const FullArticle = (props: Props) => {
if (isServer) { if (isServer) {
const result: string[] = [] const result: string[] = []
let match: RegExpMatchArray let match: RegExpMatchArray | null
while ((match = imgSrcRegExp.exec(body())) !== null) { while ((match = imgSrcRegExp.exec(body())) !== null) {
result.push(match[1]) if (match) result.push(match[1])
else break
} }
return result return result
} }
@ -139,45 +169,25 @@ export const FullArticle = (props: Props) => {
return Array.from(imageElements).map((img) => img.src) return Array.from(imageElements).map((img) => img.src)
}) })
const media = createMemo<MediaItem[]>(() => { const media = createMemo<MediaItem[]>(() => JSON.parse(props.article.media || '[]'))
try {
return JSON.parse(props.article.media)
} catch {
return []
}
})
const commentsRef: {
current: HTMLDivElement
} = { current: null }
let commentsRef: HTMLDivElement | undefined
createEffect(() => { createEffect(() => {
if (props.scrollToComments) { if (searchParams?.commentId && isReactionsLoaded()) {
scrollTo(commentsRef.current) console.debug('comment id is in link, scroll to')
} const scrollToElement =
}) document.querySelector<HTMLElement>(`[id='comment_${searchParams?.commentId}']`) ||
commentsRef ||
document.body
createEffect(() => { if (scrollToElement) {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { requestAnimationFrame(() => scrollTo(scrollToElement))
requestAnimationFrame(() => scrollTo(commentsRef.current))
changeSearchParams({ scrollTo: null })
}
})
createEffect(() => {
if (searchParams().commentId && isReactionsLoaded()) {
const commentElement = document.querySelector<HTMLElement>(
`[id='comment_${searchParams().commentId}']`
)
if (commentElement) {
requestAnimationFrame(() => scrollTo(commentElement))
} }
} }
}) })
const clickHandlers = [] const clickHandlers: { element: HTMLElement; handler: () => void }[] = []
const documentClickHandlers = [] const documentClickHandlers: ((e: MouseEvent) => void)[] = []
createEffect(() => { createEffect(() => {
if (!body()) { if (!body()) {
@ -195,7 +205,7 @@ export const FullArticle = (props: Props) => {
tooltip.classList.add(styles.tooltip) tooltip.classList.add(styles.tooltip)
const tooltipContent = document.createElement('div') const tooltipContent = document.createElement('div')
tooltipContent.classList.add(styles.tooltipContent) tooltipContent.classList.add(styles.tooltipContent)
tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value || ''
tooltip.append(tooltipContent) tooltip.append(tooltipContent)
@ -239,7 +249,7 @@ export const FullArticle = (props: Props) => {
popperInstance.update() popperInstance.update()
} }
const handleDocumentClick = (e) => { const handleDocumentClick = (e: MouseEvent) => {
if (isTooltipVisible && e.target !== element && e.target !== tooltip) { if (isTooltipVisible && e.target !== element && e.target !== tooltip) {
tooltip.style.visibility = 'hidden' tooltip.style.visibility = 'hidden'
isTooltipVisible = false isTooltipVisible = false
@ -263,14 +273,15 @@ export const FullArticle = (props: Props) => {
}) })
}) })
const openLightbox = (image) => { const openLightbox = (image: string) => {
setSelectedImage(image) setSelectedImage(image)
} }
const handleLightboxClose = () => { const handleLightboxClose = () => {
setSelectedImage() setSelectedImage('')
} }
const handleArticleBodyClick = (event) => { // biome-ignore lint/suspicious/noExplicitAny: FIXME: typing
const handleArticleBodyClick = (event: any) => {
if (event.target.tagName === 'IMG' && !event.target.dataset.disableLightbox) { if (event.target.tagName === 'IMG' && !event.target.dataset.disableLightbox) {
const src = event.target.src const src = event.target.src
openLightbox(getImageUrl(src)) openLightbox(getImageUrl(src))
@ -278,12 +289,13 @@ export const FullArticle = (props: Props) => {
} }
// Check iframes size // Check iframes size
const articleContainer: { current: HTMLElement } = { current: null } let articleContainer: HTMLElement | undefined
const updateIframeSizes = () => { const updateIframeSizes = () => {
if (!(articleContainer?.current && props.article.body)) return if (!window) return
const iframes = articleContainer?.current?.querySelectorAll('iframe') if (!(articleContainer && props.article.body)) return
const iframes = articleContainer?.querySelectorAll('iframe')
if (!iframes) return if (!iframes) return
const containerWidth = articleContainer.current?.offsetWidth const containerWidth = articleContainer?.offsetWidth
iframes.forEach((iframe) => { iframes.forEach((iframe) => {
const style = window.getComputedStyle(iframe) const style = window.getComputedStyle(iframe)
const originalWidth = iframe.getAttribute('width') || style.width.replace('px', '') const originalWidth = iframe.getAttribute('width') || style.width.replace('px', '')
@ -302,58 +314,26 @@ export const FullArticle = (props: Props) => {
}) })
} }
createEffect( onMount(() => {
on( console.debug(props.article)
() => props.article, setPages((_) => ({ comments: 0, rating: 0 }))
() => { addSeen(props.article.slug)
updateIframeSizes()
}
)
)
onMount(async () => {
install('G-LQ4B87H8C2')
await loadReactionsBy({ by: { shout: props.article.slug } })
setIsReactionsLoaded(true)
document.title = props.article.title document.title = props.article.title
updateIframeSizes()
window?.addEventListener('resize', updateIframeSizes) window?.addEventListener('resize', updateIframeSizes)
onCleanup(() => window.removeEventListener('resize', updateIframeSizes)) onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
}) })
const cover = props.article.cover ?? 'production/image/logo_image.png' const shareUrl = createMemo(() => getShareUrl({ pathname: `/${props.article.slug || ''}` }))
const ogImage = getOpenGraphImageUrl(cover, { const getAuthorName = (a: Author) =>
title: props.article.title, lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replaceAll('-', ' ')) : a.name
topic: mainTopic()?.title || '',
author: props.article?.authors[0]?.name || '',
width: 1200
})
const description = getDescription(props.article.description || body())
const ogTitle = props.article.title
const keywords = getKeywords(props.article)
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
const getAuthorName = (a: Author) => {
return lang() === 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name
}
return ( return (
<> <>
<Meta name="descprition" content={description} />
<Meta name="keywords" content={keywords} />
<Meta name="og:type" content="article" />
<Meta name="og:title" content={ogTitle} />
<Meta name="og:image" content={ogImage} />
<Meta name="og:description" content={description} />
<Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={ogTitle} />
<Meta name="twitter:description" content={description} />
<Meta name="twitter:image" content={ogImage} />
<For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For> <For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For>
<div class="wide-container"> <div class="wide-container">
<div class="row position-relative"> <div class="row position-relative">
<article <article
ref={(el) => (articleContainer.current = el)} ref={(el) => (articleContainer = el)}
class={clsx('col-md-16 col-lg-14 col-xl-12 offset-md-5', styles.articleContent)} class={clsx('col-md-16 col-lg-14 col-xl-12 offset-md-5', styles.articleContent)}
onClick={handleArticleBodyClick} onClick={handleArticleBodyClick}
> >
@ -361,20 +341,20 @@ export const FullArticle = (props: Props) => {
<Show when={props.article.layout !== 'audio'}> <Show when={props.article.layout !== 'audio'}>
<div class={styles.shoutHeader}> <div class={styles.shoutHeader}>
<Show when={mainTopic()}> <Show when={mainTopic()}>
<CardTopic title={mainTopic().title} slug={mainTopic().slug} /> <CardTopic title={mainTopic()?.title || ''} slug={mainTopic()?.slug || ''} />
</Show> </Show>
<h1>{props.article.title}</h1> <h1>{props.article.title || ''}</h1>
<Show when={props.article.subtitle}> <Show when={props.article.subtitle}>
<h4>{props.article.subtitle}</h4> <h4>{processPrepositions(props.article.subtitle || '')}</h4>
</Show> </Show>
<div class={styles.shoutAuthor}> <div class={styles.shoutAuthor}>
<For each={props.article.authors}> <For each={props.article.authors}>
{(a: Author, index) => ( {(a: Maybe<Author>, index: () => number) => (
<> <>
<Show when={index() > 0}>, </Show> <Show when={index() > 0}>, </Show>
<a href={getPagePath(router, 'author', { slug: a.slug })}>{getAuthorName(a)}</a> <A href={`/@${a?.slug}`}>{a && getAuthorName(a)}</A>
</> </>
)} )}
</For> </For>
@ -387,25 +367,29 @@ export const FullArticle = (props: Props) => {
} }
> >
<figure class="img-align-column"> <figure class="img-align-column">
<Image width={800} alt={props.article.cover_caption} src={props.article.cover} /> <Image
<figcaption innerHTML={props.article.cover_caption} /> width={800}
alt={props.article.cover_caption || ''}
src={props.article.cover || ''}
/>
<figcaption innerHTML={props.article.cover_caption || ''} />
</figure> </figure>
</Show> </Show>
</div> </div>
</Show> </Show>
<Show when={props.article.lead}> <Show when={props.article.lead}>
<section class={styles.lead} innerHTML={props.article.lead} /> <section class={styles.lead} innerHTML={processPrepositions(props.article.lead || '')} />
</Show> </Show>
<Show when={props.article.layout === 'audio'}> <Show when={props.article.layout === 'audio'}>
<AudioHeader <AudioHeader
title={props.article.title} title={props.article.title || ''}
cover={props.article.cover} cover={props.article.cover || ''}
artistData={media()?.[0]} artistData={media()?.[0]}
topic={mainTopic()} topic={mainTopic() as Topic}
/> />
<Show when={media().length > 0}> <Show when={media().length > 0}>
<div class="media-items"> <div class="media-items">
<AudioPlayer media={media()} articleSlug={props.article.slug} body={body()} /> <AudioPlayer media={media()} articleSlug={props.article.slug || ''} body={body()} />
</div> </div>
</Show> </Show>
</Show> </Show>
@ -463,11 +447,11 @@ export const FullArticle = (props: Props) => {
</div> </div>
<Popover content={t('Comment')} disabled={isActionPopupActive()}> <Popover content={t('Comment')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<div <div
class={clsx(styles.shoutStatsItem)} class={clsx(styles.shoutStatsItem)}
ref={triggerRef} ref={triggerRef}
onClick={() => scrollTo(commentsRef.current)} onClick={() => commentsRef && scrollTo(commentsRef)}
> >
<Icon name="comment" class={styles.icon} /> <Icon name="comment" class={styles.icon} />
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
@ -483,7 +467,7 @@ export const FullArticle = (props: Props) => {
<Show when={props.article.stat?.viewed}> <Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}> <div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
{t('viewsWithCount', { count: props.article.stat?.viewed })} {t('some views', { count: props.article.stat?.viewed || 0 })}
</div> </div>
</Show> </Show>
@ -494,7 +478,7 @@ export const FullArticle = (props: Props) => {
</div> </div>
<Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}> <Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<div <div
class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)} class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)}
ref={triggerRef} ref={triggerRef}
@ -509,13 +493,13 @@ export const FullArticle = (props: Props) => {
</Popover> </Popover>
<Popover content={t('Share')} disabled={isActionPopupActive()}> <Popover content={t('Share')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}> <div class={styles.shoutStatsItem} ref={triggerRef}>
<SharePopup <SharePopup
title={props.article.title} title={props.article.title}
description={description} description={props.article.description || body() || media()[0]?.body}
imageUrl={props.article.cover} imageUrl={props.article.cover || ''}
shareUrl={shareUrl} shareUrl={shareUrl()}
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)} onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={ trigger={
@ -531,22 +515,19 @@ export const FullArticle = (props: Props) => {
<Show when={canEdit()}> <Show when={canEdit()}>
<Popover content={t('Edit')}> <Popover content={t('Edit')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}> <div class={styles.shoutStatsItem} ref={triggerRef}>
<a <A href={`/edit/${props.article.id}`} class={styles.shoutStatsItemInner}>
href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}
class={styles.shoutStatsItemInner}
>
<Icon name="pencil-outline" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
</a> </A>
</div> </div>
)} )}
</Popover> </Popover>
</Show> </Show>
<FeedArticlePopup <FeedArticlePopup
canEdit={canEdit()} canEdit={Boolean(canEdit())}
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)} containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
onShareClick={() => showModal('share')} onShareClick={() => showModal('share')}
onInviteClick={() => showModal('inviteMembers')} onInviteClick={() => showModal('inviteMembers')}
@ -560,7 +541,7 @@ export const FullArticle = (props: Props) => {
/> />
</div> </div>
<Show when={isAuthenticated() && !canEdit()}> <Show when={session()?.access_token && !canEdit()}>
<div class={styles.help}> <div class={styles.help}>
<button class="button">{t('Cooperate')}</button> <button class="button">{t('Cooperate')}</button>
</div> </div>
@ -571,14 +552,14 @@ export const FullArticle = (props: Props) => {
</div> </div>
</Show> </Show>
<Show when={props.article.topics.length}> <Show when={props.article.topics?.length}>
<div class={styles.topicsList}> <div class={styles.topicsList}>
<For each={props.article.topics}> <For each={props.article.topics || []}>
{(topic) => ( {(topic) => (
<div class={styles.shoutTopic}> <div class={styles.shoutTopic}>
<a href={getPagePath(router, 'topic', { slug: topic.slug })}> <A href={`/topic/${topic?.slug || ''}`}>
{lang() === 'en' ? capitalize(topic.slug) : topic.title} {lang() === 'en' ? capitalize(topic?.slug || '') : topic?.title || ''}
</a> </A>
</div> </div>
)} )}
</For> </For>
@ -586,23 +567,23 @@ export const FullArticle = (props: Props) => {
</Show> </Show>
<div class={styles.shoutAuthorsList}> <div class={styles.shoutAuthorsList}>
<Show when={props.article.authors.length > 1}> <Show when={(props.article.authors?.length || 0) > 1}>
<h4>{t('Authors')}</h4> <h4>{t('Authors')}</h4>
</Show> </Show>
<For each={props.article.authors}> <For each={props.article.authors}>
{(a: Author) => ( {(a: Maybe<Author>) => (
<div class="col-xl-12"> <div class="col-xl-12">
<AuthorBadge iconButtons={true} showMessageButton={true} author={a} /> <AuthorBadge iconButtons={true} showMessageButton={true} author={a as Author} />
</div> </div>
)} )}
</For> </For>
</div> </div>
<div id="comments" ref={(el) => (commentsRef.current = el)}> <div id="comments" ref={(el) => (commentsRef = el)}>
<Show when={isReactionsLoaded()}> <Show when={isReactionsLoaded()}>
<CommentsTree <CommentsTree
shoutId={props.article.id} shoutId={props.article.id}
shoutSlug={props.article.slug} shoutSlug={props.article.slug}
articleAuthors={props.article.authors} articleAuthors={props.article.authors as Author[]}
/> />
</Show> </Show>
</div> </div>
@ -617,9 +598,9 @@ export const FullArticle = (props: Props) => {
</Modal> </Modal>
<ShareModal <ShareModal
title={props.article.title} title={props.article.title}
description={description} description={props.article.description || body() || media()[0]?.body}
imageUrl={props.article.cover} imageUrl={props.article.cover || ''}
shareUrl={shareUrl} shareUrl={shareUrl()}
/> />
</> </>
) )

View File

@ -28,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => {
}) })
return ( return (
<Popup {...props} variant="bordered" onVisibilityChange={(value) => setIsVisible(value)}> <Popup {...props} onVisibilityChange={(value) => setIsVisible(value)}>
<ShareLinks <ShareLinks
variant="inPopup" variant="inPopup"
title={props.title} title={props.title}

View File

@ -1,11 +1,11 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal } from 'solid-js' import { Show, createMemo, createSignal } from 'solid-js'
import { useFeed } from '~/context/feed'
import { useLocalize } from '../../context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '../../context/session' import { useSession } from '~/context/session'
import { ReactionKind, Shout } from '../../graphql/schema/core.gen' import type { Author } from '~/graphql/schema/core.gen'
import { loadShout } from '../../stores/zine/articles' import { ReactionKind, Shout } from '~/graphql/schema/core.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList' import { VotersList } from '../_shared/VotersList'
@ -19,12 +19,14 @@ interface ShoutRatingControlProps {
export const ShoutRatingControl = (props: ShoutRatingControlProps) => { export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { author, requireAuthentication } = useSession() const { loadShout } = useFeed()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() const { requireAuthentication, session } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { reactionEntities, createShoutReaction, deleteShoutReaction, loadReactionsBy } = useReactions()
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some( Object.values(reactionEntities()).some(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
r.created_by.id === author()?.id && r.created_by.id === author()?.id &&
@ -36,12 +38,12 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike)) const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const shoutRatingReactions = createMemo(() => const shoutRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter( Object.values(reactionEntities()).filter(
(r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to (r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to
) )
) )
const deleteShoutReaction = async (reactionKind: ReactionKind) => { const removeReaction = async (reactionKind: ReactionKind) => {
const reactionToDelete = Object.values(reactionEntities).find( const reactionToDelete = Object.values(reactionEntities).find(
(r) => (r) =>
r.kind === reactionKind && r.kind === reactionKind &&
@ -49,20 +51,22 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.reply_to !r.reply_to
) )
return deleteReaction(reactionToDelete.id) if (reactionToDelete) return deleteShoutReaction(reactionToDelete.id)
} }
const handleRatingChange = (isUpvote: boolean) => { const handleRatingChange = (isUpvote: boolean) => {
requireAuthentication(async () => { requireAuthentication(async () => {
setIsLoading(true) setIsLoading(true)
if (isUpvoted()) { if (isUpvoted()) {
await deleteShoutReaction(ReactionKind.Like) await removeReaction(ReactionKind.Like)
} else if (isDownvoted()) { } else if (isDownvoted()) {
await deleteShoutReaction(ReactionKind.Dislike) await removeReaction(ReactionKind.Dislike)
} else { } else {
await createReaction({ await createShoutReaction({
reaction: {
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.shout.id shout: props.shout.id
}
}) })
} }
@ -83,7 +87,10 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
</Show> </Show>
</button> </button>
<Popup trigger={<span class={styles.ratingValue}>{props.shout.stat.rating}</span>} variant="tiny"> <Popup
trigger={<span class={styles.ratingValue}>{props.shout.stat?.rating || 0}</span>}
variant="tiny"
>
<VotersList <VotersList
reactions={shoutRatingReactions()} reactions={shoutRatingReactions()}
fallbackMessage={t('This post has not been rated yet')} fallbackMessage={t('This post has not been rated yet')}

View File

@ -1,10 +1,7 @@
import { JSX, Show, createEffect } from 'solid-js' import { useSearchParams } from '@solidjs/router'
import { JSX, Show, createEffect, createMemo, on } from 'solid-js'
import { useSession } from '../../context/session' import { useSession } from '~/context/session'
import { RootSearchParams } from '../../pages/types' import { useUI } from '~/context/ui'
import { useRouter } from '../../stores/router'
import { hideModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
type Props = { type Props = {
children: JSX.Element children: JSX.Element
@ -12,15 +9,18 @@ type Props = {
} }
export const AuthGuard = (props: Props) => { export const AuthGuard = (props: Props) => {
const { isAuthenticated, isSessionLoaded } = useSession() const { session } = useSession()
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>() const author = createMemo<number>(() => session()?.user?.app_data?.profile?.id || 0)
const [, changeSearchParams] = useSearchParams()
const { hideModal } = useUI()
createEffect(() => { createEffect(
if (props.disabled) { on(
return [() => props.disabled, author],
} ([disabled, a]) => {
if (isSessionLoaded()) { if (disabled || !a) return
if (isAuthenticated()) { if (a) {
console.debug('[AuthGuard] profile is loaded')
hideModal() hideModal()
} else { } else {
changeSearchParams( changeSearchParams(
@ -28,14 +28,13 @@ export const AuthGuard = (props: Props) => {
source: 'authguard', source: 'authguard',
m: 'auth' m: 'auth'
}, },
true { replace: true }
) )
} }
} else { },
// await loadSession() { defer: true }
console.warn('session is not loaded') )
} )
})
return <Show when={(isSessionLoaded() && isAuthenticated()) || props.disabled}>{props.children}</Show> return <Show when={author() || props.disabled}>{props.children}</Show>
} }

View File

@ -1,13 +1,13 @@
.view { .view {
background: #fff;
min-height: 550px;
position: relative;
justify-content: center;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
min-height: 600px; min-height: 600px;
} }
background: var(--background-color);
min-height: 550px;
position: relative;
justify-content: center;
input { input {
font-size: 1.7rem; font-size: 1.7rem;
} }
@ -41,6 +41,10 @@
.authImage { .authImage {
@include font-size(1.5rem); @include font-size(1.5rem);
@include media-breakpoint-down(sm) {
display: none;
}
background: var(--background-color-invert) background: var(--background-color-invert)
url('https://images.discours.io/unsafe/1600x/production/image/auth-page.jpg') center no-repeat; url('https://images.discours.io/unsafe/1600x/production/image/auth-page.jpg') center no-repeat;
background-size: cover; background-size: cover;
@ -49,10 +53,6 @@
padding: 3em; padding: 3em;
position: relative; position: relative;
@include media-breakpoint-down(sm) {
display: none;
}
h2 { h2 {
text-transform: uppercase; text-transform: uppercase;
} }
@ -118,13 +118,13 @@
} }
.auth { .auth {
display: flex;
flex-direction: column;
padding: $container-padding-x;
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
padding: 4rem !important; padding: 4rem !important;
} }
display: flex;
flex-direction: column;
padding: $container-padding-x;
} }
.submitButton { .submitButton {
@ -154,17 +154,6 @@
margin-bottom: 1em; margin-bottom: 1em;
} }
.authInfo {
font-weight: 400;
font-size: smaller;
margin-top: -2em;
position: absolute;
.warn {
color: #a00;
}
}
.authForm { .authForm {
display: flex; display: flex;
flex: 1; flex: 1;
@ -221,3 +210,7 @@
line-height: 24px; line-height: 24px;
margin-bottom: 52px; margin-bottom: 52px;
} }
.submitError {
margin: -1rem 0 -2rem;
}

View File

@ -1,9 +1,6 @@
import { useSearchParams } from '@solidjs/router'
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { useLocalize } from '~/context/localize'
import { useLocalize } from '../../../../context/localize'
import { useRouter } from '../../../../stores/router'
import { AuthModalSearchParams } from '../types'
import styles from './AuthModalHeader.module.scss' import styles from './AuthModalHeader.module.scss'
type Props = { type Props = {
@ -12,15 +9,14 @@ type Props = {
export const AuthModalHeader = (props: Props) => { export const AuthModalHeader = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { searchParams } = useRouter<AuthModalSearchParams>() const [searchParams] = useSearchParams<{ source: string }>()
const { source } = searchParams()
const generateModalTextsFromSource = ( const generateModalTextsFromSource = (
modalType: 'login' | 'register' modalType: 'login' | 'register'
): { title: string; description: string } => { ): { title: string; description: string } => {
const title = modalType === 'login' ? 'Welcome to Discours' : 'Create account' const title = modalType === 'login' ? 'Welcome to Discours' : 'Sign up'
switch (source) { switch (searchParams?.source) {
case 'create': { case 'create': {
return { return {
title: t(`${title} to publish articles`), title: t(`${title} to publish articles`),
@ -31,7 +27,7 @@ export const AuthModalHeader = (props: Props) => {
return { return {
title: t(`${title} to add to your bookmarks`), title: t(`${title} to add to your bookmarks`),
description: t( description: t(
'In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to' 'In bookmarks, you can save favorite discussions and materials that you want to return to'
) )
} }
} }
@ -39,7 +35,7 @@ export const AuthModalHeader = (props: Props) => {
return { return {
title: t(`${title} to participate in discussions`), title: t(`${title} to participate in discussions`),
description: t( description: t(
"You&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;new responses" "You ll be able to participate in discussions, rate others' comments and learn about new responses"
) )
} }
} }
@ -47,7 +43,7 @@ export const AuthModalHeader = (props: Props) => {
return { return {
title: t(`${title} to subscribe`), title: t(`${title} to subscribe`),
description: t( description: t(
'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed' 'This way you ll be able to subscribe to authors, interesting topics and customize your feed'
) )
} }
} }
@ -55,7 +51,7 @@ export const AuthModalHeader = (props: Props) => {
return { return {
title: t(`${title} to subscribe to new publications`), title: t(`${title} to subscribe to new publications`),
description: t( description: t(
'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed' 'This way you ll be able to subscribe to authors, interesting topics and customize your feed'
) )
} }
} }
@ -63,7 +59,7 @@ export const AuthModalHeader = (props: Props) => {
return { return {
title: t(`${title} to vote`), title: t(`${title} to vote`),
description: t( description: t(
'This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted' 'This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted'
) )
} }
} }

View File

@ -1,15 +1,12 @@
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { JSX, Show, createSignal } from 'solid-js' import { JSX, Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '../../../context/session' import { useSession } from '~/context/session'
import { useRouter } from '../../../stores/router' import { useUI } from '~/context/ui'
import { hideModal } from '../../../stores/ui'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import { useSearchParams } from '@solidjs/router'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
type FormFields = { type FormFields = {
@ -19,27 +16,27 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>> type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
export const ChangePasswordForm = () => { export const ChangePasswordForm = () => {
const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>() const [searchParams, changeSearchParams] = useSearchParams<{ token?: string }>()
const { hideModal } = useUI()
const { t } = useLocalize() const { t } = useLocalize()
const { changePassword } = useSession() const { changePassword } = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [newPassword, setNewPassword] = createSignal<string>() const [newPassword, setNewPassword] = createSignal<string>('')
const [passwordError, setPasswordError] = createSignal<string>() const [passwordError, setPasswordError] = createSignal<string>('')
const [isSuccess, setIsSuccess] = createSignal(false) const [isSuccess, setIsSuccess] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null } let authFormRef: HTMLFormElement | undefined
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
event.preventDefault() event.preventDefault()
setIsSubmitting(true) setIsSubmitting(true)
if (newPassword()) { if (!newPassword()) return
await changePassword(newPassword(), searchParams()?.token) if (searchParams?.token) changePassword(newPassword(), searchParams.token)
setTimeout(() => { setTimeout(() => {
setIsSubmitting(false) setIsSubmitting(false)
setIsSuccess(true) setIsSuccess(true)
}, 1000) }, 1000)
} }
}
const handlePasswordInput = (value: string) => { const handlePasswordInput = (value: string) => {
setNewPassword(value) setNewPassword(value)
@ -56,15 +53,10 @@ export const ChangePasswordForm = () => {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
class={clsx(styles.authForm, styles.authFormForgetPassword)} class={clsx(styles.authForm, styles.authFormForgetPassword)}
ref={(el) => (authFormRef.current = el)} ref={(el) => (authFormRef = el)}
> >
<div> <div>
<h4>{t('Enter a new password')}</h4> <h4>{t('Enter a new password')}</h4>
<div class={styles.authSubtitle}>
{t(
'Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password'
)}
</div>
<Show when={validationErrors()}> <Show when={validationErrors()}>
<div>{validationErrors().password}</div> <div>{validationErrors().password}</div>
</Show> </Show>

View File

@ -1,35 +1,42 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createEffect, createSignal } from 'solid-js' import { Show, createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '../../../context/session' import { useSession } from '~/context/session'
import { useRouter } from '../../../stores/router' import { useUI } from '~/context/ui'
import { hideModal } from '../../../stores/ui'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { useSearchParams } from '@solidjs/router'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
export type ConfirmEmailSearchParams = {
access_token?: string
token?: string
}
export const EmailConfirm = () => { export const EmailConfirm = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { changeSearchParams } = useRouter() const { hideModal } = useUI()
const [, changeSearchParams] = useSearchParams<ConfirmEmailSearchParams>()
const { session, authError } = useSession() const { session, authError } = useSession()
const [emailConfirmed, setEmailConfirmed] = createSignal(false) const [emailConfirmed, setEmailConfirmed] = createSignal(false)
createEffect(() => { createEffect(() => {
const e = session()?.user?.email const email = session()?.user?.email
const v = session()?.user?.email_verified const isVerified = session()?.user?.email_verified
if (e) {
setEmail(e.toLowerCase())
if (v) setEmailConfirmed(v)
if (authError()) {
changeSearchParams({}, true)
}
}
})
createEffect(() => { if (email) {
if (authError()) console.debug('[AuthModal.EmailConfirm] auth error:', authError()) setEmail(email.toLowerCase())
if (isVerified) setEmailConfirmed(isVerified)
if (authError()) {
changeSearchParams({}, { replace: true })
}
}
if (authError()) {
console.debug('[AuthModal.EmailConfirm] auth error:', authError())
}
}) })
return ( return (

View File

@ -1,20 +1,17 @@
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { JSX, Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '../../../context/session' import { useSession } from '~/context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar, useUI } from '~/context/ui'
import { useRouter } from '../../../stores/router' import { validateEmail } from '~/utils/validate'
import { hideModal } from '../../../stores/ui'
import { validateEmail } from '../../../utils/validateEmail'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { useSearchParams } from '@solidjs/router'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
type FormFields = { type FormFields = {
@ -25,18 +22,18 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string>> type ValidationErrors = Partial<Record<keyof FormFields, string>>
export const LoginForm = () => { export const LoginForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>() const { hideModal } = useUI()
const [, setSearchParams] = useSearchParams()
const { t } = useLocalize() const { t } = useLocalize()
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal<string | JSX.Element>()
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
// TODO: better solution for interactive error messages // FIXME: use signal or remove
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) const [_isLinkSent, setIsLinkSent] = createSignal(false)
const [isLinkSent, setIsLinkSent] = createSignal(false) let authFormRef: HTMLFormElement
const authFormRef: { current: HTMLFormElement } = { current: null }
const { showSnackbar } = useSnackbar() const { showSnackbar } = useSnackbar()
const { signIn } = useSession() const { signIn, authError } = useSession()
const handleEmailInput = (newEmail: string) => { const handleEmailInput = (newEmail: string) => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
@ -52,88 +49,94 @@ export const LoginForm = () => {
event.preventDefault() event.preventDefault()
setIsLinkSent(true) setIsLinkSent(true)
setIsEmailNotConfirmed(false) setSubmitError()
setSubmitError('') setSearchParams({ mode: 'send-confirm-email' })
changeSearchParams({ mode: 'send-reset-link' })
// NOTE: temporary solution, needs logic in authorizer
/* FIXME:
const { authorizer } = useSession()
const result = await authorizer().verifyEmail({ token })
if (!result) setSubmitError('cant sign send link')
*/
} }
const preSendValidate = async (value: string, type: 'email' | 'password'): Promise<boolean> => {
if (type === 'email') {
if (value === '' || !validateEmail(value)) {
setValidationErrors((prev) => ({
...prev,
email: t('Invalid email')
}))
return false
}
} else if (type === 'password') {
if (value === '') {
setValidationErrors((prev) => ({
...prev,
password: t('Please enter password')
}))
return false
}
}
return true
}
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
event.preventDefault() event.preventDefault()
await preSendValidate(email(), 'email')
await preSendValidate(password(), 'password')
setIsLinkSent(false) setIsLinkSent(false)
setIsEmailNotConfirmed(false) setSubmitError()
setSubmitError('')
const newValidationErrors: ValidationErrors = {} if (Object.keys(validationErrors()).length > 0) {
authFormRef
const validateAndSetError = (field, message) => { .querySelector<HTMLInputElement>(`input[name="${Object.keys(validationErrors())[0]}"]`)
if (!field()) {
newValidationErrors[field.name] = t(message)
}
}
validateAndSetError(email, 'Please enter email')
validateAndSetError(() => validateEmail(email()), 'Invalid email')
validateAndSetError(password, 'Please enter password')
if (Object.keys(newValidationErrors).length > 0) {
setValidationErrors(newValidationErrors)
authFormRef.current
.querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`)
?.focus() ?.focus()
return return
} }
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const { errors } = await signIn({ email: email(), password: password() }) const success = await signIn({ email: email(), password: password() })
if (errors?.length > 0) { if (!success) {
if (errors.some((error) => error.message.includes('bad user credentials'))) { switch (authError()) {
case 'user has not signed up email & password':
case 'bad user credentials': {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
password: t('Something went wrong, check email and password') password: t('Something went wrong, check email and password')
})) }))
} else { break
setSubmitError(t('Error')) }
case 'user not found': {
setValidationErrors((prev) => ({ ...prev, email: t('User was not found') }))
break
}
case 'email not verified': {
setValidationErrors((prev) => ({ ...prev, email: t('This email is not verified') }))
break
}
default:
setSubmitError(
<div class={styles.info}>
{t('Error', authError())}
{'. '}
<span class={'link'} onClick={handleSendLinkAgainClick}>
{t('Send link again')}
</span>
</div>
)
} }
return
} }
hideModal() hideModal()
showSnackbar({ body: t('Welcome!') }) showSnackbar({ body: t('Welcome!') })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
setSubmitError(error.message) setSubmitError(authError())
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
} }
return ( return (
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}> <form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef = el)}>
<div> <div>
<AuthModalHeader modalType="login" /> <AuthModalHeader modalType="login" />
<Show when={submitError()}>
<div class={styles.authInfo}>
<div class={styles.warn}>{submitError()}</div>
<Show when={isEmailNotConfirmed()}>
<span class={'link'} onClick={handleSendLinkAgainClick}>
{t('Send link again')}
</span>
</Show>
</div>
</Show>
<Show when={isLinkSent()}>
<div class={styles.authInfo}>{t('Link sent, check your email')}</div>
</Show>
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email 'pretty-form__item--error': validationErrors().email
@ -154,11 +157,14 @@ export const LoginForm = () => {
</Show> </Show>
</div> </div>
<PasswordField variant={'login'} onInput={(value) => handlePasswordInput(value)} /> <PasswordField
<Show when={validationErrors().password}> variant={'login'}
<div class={styles.validationError} style={{ position: 'static', 'font-size': '1.4rem' }}> setError={validationErrors().password}
{validationErrors().password} onBlur={(value) => handlePasswordInput(value)}
</div> />
<Show when={submitError()}>
<div class={clsx('form-message--error', styles.validationError)}>{submitError()}</div>
</Show> </Show>
<div> <div>
@ -170,12 +176,12 @@ export const LoginForm = () => {
<span <span
class="link" class="link"
onClick={() => onClick={() =>
changeSearchParams({ setSearchParams({
mode: 'send-reset-link' mode: 'send-reset-link'
}) })
} }
> >
{t('Set the new password')} {t('Forgot password?')}
</span> </span>
</div> </div>
</div> </div>
@ -187,7 +193,7 @@ export const LoginForm = () => {
<span <span
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParams({ setSearchParams({
mode: 'register' mode: 'register'
}) })
} }

View File

@ -26,16 +26,16 @@
line-height: 16px; line-height: 16px;
margin-top: 0.3em; margin-top: 0.3em;
/* Red/500 */
color: orange;
&.registerPassword { &.registerPassword {
margin-bottom: -32px; margin-bottom: -32px;
} }
/* Red/500 */
color: #d00820;
a { a {
color: #d00820; color: orange;
border-color: #d00820; border-color: orange;
&:hover { &:hover {
color: var(--default-color-invert); color: var(--default-color-invert);

View File

@ -1,9 +1,7 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createEffect, createSignal, on } from 'solid-js' import { Show, createEffect, createSignal, on } from 'solid-js'
import { useLocalize } from '~/context/localize'
import { useLocalize } from '../../../../context/localize' import { Icon } from '../../_shared/Icon'
import { Icon } from '../../../_shared/Icon'
import styles from './PasswordField.module.scss' import styles from './PasswordField.module.scss'
type Props = { type Props = {
@ -11,21 +9,26 @@ type Props = {
disabled?: boolean disabled?: boolean
placeholder?: string placeholder?: string
errorMessage?: (error: string) => void errorMessage?: (error: string) => void
onInput: (value: string) => void setError?: string
onInput?: (value: string) => void
onBlur?: (value: string) => void
variant?: 'login' | 'registration' variant?: 'login' | 'registration'
disableAutocomplete?: boolean disableAutocomplete?: boolean
noValidate?: boolean
onFocus?: () => void
value?: string
} }
const minLength = 8
const hasNumber = /\d/
const hasSpecial = /[!#$%&*@^]/
export const PasswordField = (props: Props) => { export const PasswordField = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const [showPassword, setShowPassword] = createSignal(false) const [showPassword, setShowPassword] = createSignal(false)
const [error, setError] = createSignal<string>() const [error, setError] = createSignal<string>()
const validatePassword = (passwordToCheck) => { const validatePassword = (passwordToCheck: string) => {
const minLength = 8
const hasNumber = /\d/
const hasSpecial = /[!#$%&*@^]/
if (passwordToCheck.length < minLength) { if (passwordToCheck.length < minLength) {
return t('Password should be at least 8 characters') return t('Password should be at least 8 characters')
} }
@ -35,12 +38,20 @@ export const PasswordField = (props: Props) => {
if (!hasSpecial.test(passwordToCheck)) { if (!hasSpecial.test(passwordToCheck)) {
return t('Password should contain at least one special character: !@#$%^&*') return t('Password should contain at least one special character: !@#$%^&*')
} }
return null return null
} }
const handleInputChange = (value) => { const handleInputBlur = (value: string) => {
props.onInput(value) if (props.variant === 'login' && props.onBlur) {
props.onBlur(value)
return
}
if (value.length < 1) {
return
}
props.onInput?.(value)
if (!props.noValidate) {
const errorValue = validatePassword(value) const errorValue = validatePassword(value)
if (errorValue) { if (errorValue) {
setError(errorValue) setError(errorValue)
@ -48,32 +59,24 @@ export const PasswordField = (props: Props) => {
setError() setError()
} }
} }
}
createEffect( createEffect(on(error, (er) => er && props.errorMessage?.(er), { defer: true }))
on( createEffect(() => setError(props.setError))
() => error(),
() => {
props.errorMessage?.(error())
},
{ defer: true }
)
)
return ( return (
<div class={clsx(styles.PassportField, props.class)}> <div class={clsx(styles.PassportField, props.class)}>
<div <div class="pretty-form__item">
class={clsx('pretty-form__item', {
'pretty-form__item--error': error() && props.variant !== 'login'
})}
>
<input <input
id="password" id="password"
name="password" name="password"
disabled={props.disabled} disabled={props.disabled}
onFocus={props.onFocus}
value={props.value ? props.value : ''}
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'} autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
type={showPassword() ? 'text' : 'password'} type={showPassword() ? 'text' : 'password'}
placeholder={props.placeholder || t('Password')} placeholder={props.placeholder || t('Password')}
onInput={(event) => handleInputChange(event.currentTarget.value)} onBlur={(event) => handleInputBlur(event.currentTarget.value)}
/> />
<label for="password">{t('Password')}</label> <label for="password">{t('Password')}</label>
<button <button
@ -83,8 +86,14 @@ export const PasswordField = (props: Props) => {
> >
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} /> <Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
</button> </button>
<Show when={error() && props.variant !== 'login'}> <Show when={error()}>
<div class={clsx(styles.registerPassword, styles.validationError)}>{error()}</div> <div
class={clsx(styles.registerPassword, styles.validationError, {
'form-message--error': props.setError
})}
>
{error()}
</div>
</Show> </Show>
</div> </div>
</div> </div>

View File

@ -1,21 +1,17 @@
import { clsx } from 'clsx'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { Show, createMemo, createSignal } from 'solid-js' import { Show, createMemo, createSignal } from 'solid-js'
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { validateEmail } from '../../../utils/validateEmail'
import { useSearchParams } from '@solidjs/router'
import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session'
import { useUI } from '~/context/ui'
import { validateEmail } from '~/utils/validate'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { GenericResponse } from '@authorizerdev/authorizer-js'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
type EmailStatus = 'not verified' | 'verified' | 'registered' | '' type EmailStatus = 'not verified' | 'verified' | 'registered' | ''
@ -28,15 +24,13 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>> type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
const handleEmailInput = (newEmail: string) => {
setEmail(newEmail.toLowerCase())
}
export const RegisterForm = () => { export const RegisterForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>() const [, changeSearchParams] = useSearchParams()
const { hideModal } = useUI()
const { t } = useLocalize() const { t } = useLocalize()
const { signUp, isRegistered, resendVerifyEmail } = useSession() const { signUp, isRegistered, resendVerifyEmail } = useSession()
const [submitError, setSubmitError] = createSignal('') // FIXME: use submit error data or remove signal
const [_submitError, setSubmitError] = createSignal('')
const [fullName, setFullName] = createSignal('') const [fullName, setFullName] = createSignal('')
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
@ -45,7 +39,7 @@ export const RegisterForm = () => {
const [passwordError, setPasswordError] = createSignal<string>() const [passwordError, setPasswordError] = createSignal<string>()
const [emailStatus, setEmailStatus] = createSignal<string>('') const [emailStatus, setEmailStatus] = createSignal<string>('')
const authFormRef: { current: HTMLFormElement } = { current: null } let authFormRef: HTMLFormElement
const handleNameInput = (newName: string) => { const handleNameInput = (newName: string) => {
setFullName(newName) setFullName(newName)
@ -53,7 +47,6 @@ export const RegisterForm = () => {
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
event.preventDefault() event.preventDefault()
if (passwordError()) { if (passwordError()) {
setValidationErrors((errors) => ({ ...errors, password: passwordError() })) setValidationErrors((errors) => ({ ...errors, password: passwordError() }))
} else { } else {
@ -85,11 +78,10 @@ export const RegisterForm = () => {
const isValid = createMemo(() => Object.keys(newValidationErrors).length === 0) const isValid = createMemo(() => Object.keys(newValidationErrors).length === 0)
if (!isValid()) { if (!isValid() && authFormRef) {
authFormRef.current authFormRef
.querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`) .querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`)
.focus() ?.focus()
return return
} }
setIsSubmitting(true) setIsSubmitting(true)
@ -99,11 +91,10 @@ export const RegisterForm = () => {
email: cleanEmail, email: cleanEmail,
password: password(), password: password(),
confirm_password: password(), confirm_password: password(),
redirect_uri: window.location.origin redirect_uri: window?.location?.origin || ''
} }
const { errors } = await signUp(opts) const success = await signUp(opts)
if (errors) return setIsSuccess(success)
setIsSuccess(true)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
@ -111,17 +102,18 @@ export const RegisterForm = () => {
} }
} }
const handleResendLink = async (_ev) => { // biome-ignore lint/suspicious/noExplicitAny: <explanation>
const response: GenericResponse = await resendVerifyEmail({ const handleResendLink = async (_ev: any) => {
const success: boolean = await resendVerifyEmail({
email: email(), email: email(),
identifier: 'basic_signup' identifier: 'basic_signup'
}) })
setIsSuccess(response?.message === 'Verification email has been sent. Please check your inbox') setIsSuccess(success)
} }
const handleCheckEmailStatus = (status: EmailStatus | string) => { const handleCheckEmailStatus = (status: EmailStatus | string) => {
switch (status) { switch (status) {
case 'not verified': case 'not verified': {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
email: ( email: (
@ -134,37 +126,42 @@ export const RegisterForm = () => {
) )
})) }))
break break
}
case 'verified': case 'verified': {
setValidationErrors((prev) => ({ setValidationErrors((_prev) => ({
email: ( email: (
<> <>
{t('This email is verified')}. {t('You can')} {t('This email is registered')}. {t('try')}
{', '}
<span class="link" onClick={() => changeSearchParams({ mode: 'login' })}> <span class="link" onClick={() => changeSearchParams({ mode: 'login' })}>
{t('enter')} {t('Enter').toLocaleLowerCase()}
</span> </span>
</> </>
) )
})) }))
break break
case 'registered': }
case 'registered': {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
email: ( email: (
<> <>
{t('This email is registered')}. {t('You can')}{' '} {t('This email is registered')}
{'. '}
<span class="link" onClick={() => changeSearchParams({ mode: 'send-reset-link' })}> <span class="link" onClick={() => changeSearchParams({ mode: 'send-reset-link' })}>
{t('Set the new password').toLocaleLowerCase()} {t('Set the new password')}
</span> </span>
</> </>
) )
})) }))
break break
default: }
default: {
console.info('[RegisterForm] email is not registered') console.info('[RegisterForm] email is not registered')
break break
} }
} }
}
const handleEmailBlur = async () => { const handleEmailBlur = async () => {
if (validateEmail(email())) { if (validateEmail(email())) {
@ -174,17 +171,18 @@ export const RegisterForm = () => {
} }
} }
const handleEmailInput = (newEmail: string) => {
setEmailStatus('')
setValidationErrors({})
setEmail(newEmail.toLowerCase())
}
return ( return (
<> <>
<Show when={!isSuccess()}> <Show when={!isSuccess()}>
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}> <form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef = el)}>
<div> <div>
<AuthModalHeader modalType="register" /> <AuthModalHeader modalType="register" />
<Show when={submitError()}>
<div class={styles.authInfo}>
<div class={styles.warn}>{submitError()}</div>
</div>
</Show>
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().fullName 'pretty-form__item--error': validationErrors().fullName
@ -196,7 +194,7 @@ export const RegisterForm = () => {
disabled={Boolean(emailStatus())} disabled={Boolean(emailStatus())}
placeholder={t('Full name')} placeholder={t('Full name')}
autocomplete="one-time-code" autocomplete="one-time-code"
onInput={(event) => handleNameInput(event.currentTarget.value)} onChange={(event) => handleNameInput(event.currentTarget.value)}
/> />
<label for="name">{t('Full name')}</label> <label for="name">{t('Full name')}</label>
<Show when={validationErrors().fullName && !emailStatus()}> <Show when={validationErrors().fullName && !emailStatus()}>
@ -219,16 +217,18 @@ export const RegisterForm = () => {
onBlur={handleEmailBlur} onBlur={handleEmailBlur}
/> />
<label for="email">{t('Email')}</label> <label for="email">{t('Email')}</label>
<Show when={validationErrors().email || emailStatus()}>
<div class={clsx(styles.validationError, { info: Boolean(emailStatus()) })}> <div class={clsx(styles.validationError, { info: Boolean(emailStatus()) })}>
{validationErrors().email} {validationErrors().email}
</div> </div>
</Show>
</div> </div>
<PasswordField <PasswordField
disableAutocomplete={true} disableAutocomplete={true}
disabled={Boolean(emailStatus())} disabled={Boolean(emailStatus())}
errorMessage={(err) => setPasswordError(err)} errorMessage={(err) => !emailStatus() && setPasswordError(err)}
onInput={(value) => setPassword(value)} onInput={(value) => setPassword(emailStatus() ? '' : value)}
/> />
<div> <div>
@ -261,6 +261,7 @@ export const RegisterForm = () => {
</form> </form>
</Show> </Show>
<Show when={isSuccess()}> <Show when={isSuccess()}>
<div style={{ 'justify-content': 'center' }}>
<div class={styles.title}>{t('Almost done! Check your email.')}</div> <div class={styles.title}>{t('Almost done! Check your email.')}</div>
<div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div> <div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div>
<div> <div>
@ -268,6 +269,7 @@ export const RegisterForm = () => {
{t('Back to main page')} {t('Back to main page')}
</button> </button>
</div> </div>
</div>
</Show> </Show>
</> </>
) )

View File

@ -0,0 +1,26 @@
import { clsx } from 'clsx'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import styles from './AuthModal.module.scss'
export const SendEmailConfirm = () => {
const { hideModal } = useUI()
const { t } = useLocalize()
return (
<div
style={{
'align-items': 'center',
'justify-content': 'center'
}}
>
<div class={styles.text}>{t('Link sent, check your email')}</div>
<div>
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
{t('Go to main page')}
</button>
</div>
</div>
)
}

View File

@ -1,15 +1,12 @@
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { JSX, Show, createSignal } from 'solid-js' import { JSX, Show, createSignal, onMount } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useRouter } from '../../../stores/router'
import { validateEmail } from '../../../utils/validateEmail'
import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session'
import { validateEmail } from '~/utils/validate'
import { email, setEmail } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import { useSearchParams } from '@solidjs/router'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
type FormFields = { type FormFields = {
@ -19,7 +16,7 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>> type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
export const SendResetLinkForm = () => { export const SendResetLinkForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>() const [, changeSearchParams] = useSearchParams()
const { t } = useLocalize() const { t } = useLocalize()
const handleEmailInput = (newEmail: string) => { const handleEmailInput = (newEmail: string) => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
@ -29,7 +26,7 @@ export const SendResetLinkForm = () => {
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [isUserNotFound, setIsUserNotFound] = createSignal(false) const [isUserNotFound, setIsUserNotFound] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null } let authFormRef: HTMLFormElement
const [message, setMessage] = createSignal<string>('') const [message, setMessage] = createSignal<string>('')
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
@ -47,24 +44,25 @@ export const SendResetLinkForm = () => {
const isValid = Object.keys(newValidationErrors).length === 0 const isValid = Object.keys(newValidationErrors).length === 0
if (!isValid) { if (!isValid) {
authFormRef.current authFormRef
.querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`) ?.querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`)
.focus() ?.focus()
return return
} }
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const { data, errors } = await forgotPassword({ const result = await forgotPassword({
email: email(), email: email(),
redirect_uri: window.location.origin redirect_uri: window?.location?.origin || ''
}) })
console.debug('[SendResetLinkForm] authorizer response:', data) if (result) {
if (errors?.some((error) => error.message.includes('bad user credentials'))) { setMessage(result || '')
setIsUserNotFound(true) } else {
console.warn('[SendResetLinkForm] forgot password mutation failed')
setIsUserNotFound(false)
} }
if (data.message) setMessage(data.message)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
@ -72,15 +70,25 @@ export const SendResetLinkForm = () => {
} }
} }
onMount(() => {
if (email()) {
console.info('[SendResetLinkForm] email detected')
}
})
return ( return (
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
class={clsx(styles.authForm, styles.authFormForgetPassword)} class={clsx(styles.authForm, styles.authFormForgetPassword)}
ref={(el) => (authFormRef.current = el)} ref={(el) => (authFormRef = el)}
> >
<div> <div>
<h4>{t('Set the new password')}</h4> <h4>{t('Forgot password?')}</h4>
<div class={styles.authSubtitle}>{t(message()) || t('Please give us your email address')}</div> <Show when={!message()}>
<div class={styles.authSubtitle}>
{t("It's OK. Just enter your email to receive a link to change your password")}
</div>
</Show>
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email 'pretty-form__item--error': validationErrors().email
@ -94,9 +102,8 @@ export const SendResetLinkForm = () => {
type="email" type="email"
value={email()} value={email()}
placeholder={t('Email')} placeholder={t('Email')}
onInput={(event) => handleEmailInput(event.currentTarget.value)} onChange={(event) => handleEmailInput(event.currentTarget.value)}
/> />
<label for="email">{t('Email')}</label> <label for="email">{t('Email')}</label>
<Show when={isUserNotFound()}> <Show when={isUserNotFound()}>
<div class={styles.validationError}> <div class={styles.validationError}>
@ -105,7 +112,7 @@ export const SendResetLinkForm = () => {
class={'link'} class={'link'}
onClick={() => onClick={() =>
changeSearchParams({ changeSearchParams({
mode: 'login' mode: 'register'
}) })
} }
> >
@ -117,14 +124,15 @@ export const SendResetLinkForm = () => {
<div class={styles.validationError}>{validationErrors().email}</div> <div class={styles.validationError}>{validationErrors().email}</div>
</Show> </Show>
</div> </div>
<Show when={!message()} fallback={<div class={styles.authSubtitle}>{t(message())}</div>}>
<>
<div style={{ 'margin-top': '5rem' }}> <div style={{ 'margin-top': '5rem' }}>
<button <button
class={clsx('button', styles.submitButton)} class={clsx('button', styles.submitButton)}
disabled={isSubmitting() || Boolean(message())} disabled={isSubmitting() || Boolean(message())}
type="submit" type="submit"
> >
{isSubmitting() ? '...' : t('Send')} {isSubmitting() ? '...' : t('Restore password')}
</button> </button>
</div> </div>
<div class={styles.authControl}> <div class={styles.authControl}>
@ -139,6 +147,8 @@ export const SendResetLinkForm = () => {
{t('I know the password')} {t('I know the password')}
</span> </span>
</div> </div>
</>
</Show>
</div> </div>
</form> </form>
) )

View File

@ -1,9 +1,7 @@
import { For } from 'solid-js' import { For } from 'solid-js'
import { useLocalize } from '~/context/localize'
import { useLocalize } from '../../../context/localize' import { useSession } from '~/context/session'
import { useSession } from '../../../context/session'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import styles from './SocialProviders.module.scss' import styles from './SocialProviders.module.scss'
export const PROVIDERS = ['facebook', 'google', 'github'] // 'vk' | 'telegram' export const PROVIDERS = ['facebook', 'google', 'github'] // 'vk' | 'telegram'
@ -18,7 +16,11 @@ export const SocialProviders = () => {
<div class={styles.social}> <div class={styles.social}>
<For each={PROVIDERS}> <For each={PROVIDERS}>
{(provider) => ( {(provider) => (
<button class={styles[provider]} onClick={(_e) => oauth(provider)}> <button
type="button"
class={styles[provider as keyof typeof styles]}
onClick={(_e) => oauth(provider)}
>
<Icon name={provider} /> <Icon name={provider} />
</button> </button>
)} )}

View File

@ -0,0 +1 @@
export { SocialProviders } from './SocialProviders'

View File

@ -1,55 +1,69 @@
import type { AuthModalMode, AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Component, Show, createEffect, createMemo } from 'solid-js' import { Component, Show, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { useRouter } from '../../../stores/router' import { ModalSource, useUI } from '~/context/ui'
import { hideModal } from '../../../stores/ui' import { isMobile } from '~/lib/mediaQuery'
import { isMobile } from '../../../utils/media-query'
import { ChangePasswordForm } from './ChangePasswordForm' import { ChangePasswordForm } from './ChangePasswordForm'
import { EmailConfirm } from './EmailConfirm' import { EmailConfirm } from './EmailConfirm'
import { LoginForm } from './LoginForm' import { LoginForm } from './LoginForm'
import { RegisterForm } from './RegisterForm' import { RegisterForm } from './RegisterForm'
import { SendEmailConfirm } from './SendEmailConfirm'
import { SendResetLinkForm } from './SendResetLinkForm' import { SendResetLinkForm } from './SendResetLinkForm'
import { useSearchParams } from '@solidjs/router'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
export type AuthModalMode =
| 'login'
| 'register'
| 'confirm-email'
| 'send-confirm-email'
| 'send-reset-link'
| 'change-password'
export type AuthModalSearchParams = {
mode: AuthModalMode
source?: ModalSource
token?: string
}
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = { const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
login: LoginForm, login: LoginForm,
register: RegisterForm, register: RegisterForm,
'send-reset-link': SendResetLinkForm, 'send-reset-link': SendResetLinkForm,
'confirm-email': EmailConfirm, 'confirm-email': EmailConfirm,
'send-confirm-email': SendEmailConfirm,
'change-password': ChangePasswordForm 'change-password': ChangePasswordForm
} }
export const AuthModal = () => { export const AuthModal = () => {
const rootRef: { current: HTMLDivElement } = { current: null } let rootRef: HTMLDivElement | null
const { t } = useLocalize() const { t } = useLocalize()
const { searchParams } = useRouter<AuthModalSearchParams>() const [searchParams] = useSearchParams<AuthModalSearchParams>()
const { source } = searchParams() const { hideModal } = useUI()
const mode = createMemo(() => {
const mode = createMemo<AuthModalMode>(() => { return (
return AUTH_MODAL_MODES[searchParams().mode] ? searchParams().mode : 'login' AUTH_MODAL_MODES[searchParams?.mode as AuthModalMode] ? searchParams?.mode : 'login'
) as AuthModalMode
}) })
createEffect((oldMode) => { createEffect((oldMode) => {
if (oldMode !== mode() && !isMobile()) { if (oldMode !== mode() && !isMobile()) {
rootRef.current?.querySelector('input')?.focus() rootRef?.querySelector('input')?.focus()
} }
}, null) }, null)
return ( return (
<div <div
ref={(el) => (rootRef.current = el)} ref={(el) => (rootRef = el)}
class={clsx(styles.view, { class={clsx(styles.view, {
row: !source, row: !searchParams?.source,
[styles.signUp]: mode() === 'register' || mode() === 'confirm-email' [styles.signUp]: mode() === 'register' || mode() === 'confirm-email'
})} })}
> >
<Show when={!source}> <Show when={!searchParams?.source}>
<div class={clsx('col-md-12 d-none d-md-flex', styles.authImage)}> <div class={clsx('col-md-12 d-none d-md-flex', styles.authImage)}>
<div <div
class={styles.authImageText} class={styles.authImageText}
@ -61,31 +75,30 @@ export const AuthModal = () => {
{t( {t(
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine' 'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine'
)} )}
.&nbsp; . {t('New stories and more are waiting for you every day!')}
{t('New stories every day and even more!')}
</p> </p>
</div> </div>
<p class={styles.disclaimer}> <p class={styles.disclaimer}>
{t('By signing up you agree with our')}{' '} {t('By signing up you agree with our')}{' '}
<a <a
href="/about/terms-of-use" href="/terms"
onClick={() => { onClick={() => {
hideModal() hideModal()
}} }}
> >
{t('terms of use')} {t('terms of use')}
</a> </a>
, {t('personal data usage and email notifications')}. , {t('to process personal data and receive email notifications')}.
</p> </p>
</div> </div>
</div>{' '} </div>{' '}
</Show> </Show>
<div <div
class={clsx(styles.auth, { class={clsx(styles.auth, {
'col-md-12': !source 'col-md-12': !searchParams?.source
})} })}
> >
<Dynamic component={AUTH_MODAL_MODES[mode()]} /> <Dynamic component={AUTH_MODAL_MODES[mode() as AuthModalMode]} />
</div> </div>
</div> </div>
) )

View File

@ -1,4 +1,8 @@
.AuthorBadge { .AuthorBadge {
@include media-breakpoint-down(md) {
text-align: left;
}
align-items: flex-start; align-items: flex-start;
display: flex; display: flex;
gap: 1rem; gap: 1rem;
@ -12,34 +16,30 @@
} }
} }
@include media-breakpoint-down(md) { .basicInfo {
text-align: left; @include media-breakpoint-down(sm) {
flex: 0 100%;
} }
.basicInfo {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
flex: 0 calc(100% - 5.2rem); flex: 0 calc(100% - 5.2rem);
gap: 1rem; gap: 1rem;
@include media-breakpoint-down(sm) {
flex: 0 100%;
}
} }
.info { .info {
@include font-size(1.4rem); @include font-size(1.4rem);
@include media-breakpoint-up(sm) {
flex: 1 100%;
}
border: none; border: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
line-height: 1.3; line-height: 1.3;
@include media-breakpoint-up(sm) {
flex: 1 100%;
}
&:hover { &:hover {
background: unset; background: unset;
} }
@ -60,20 +60,16 @@
.bio { .bio {
@include font-size(1.2rem); @include font-size(1.2rem);
color: var(--black-400);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1rem;
color: var(--black-400);
font-weight: 500; font-weight: 500;
gap: 1rem;
max-width: 100%;
word-break: break-word;
} }
.actions { .actions {
flex: 0 20%;
display: flex;
flex-direction: row;
margin-left: 5.2rem;
gap: 1rem;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
margin-left: 0; margin-left: 0;
} }
@ -88,6 +84,12 @@
padding-left: 1rem; padding-left: 1rem;
text-align: right; text-align: right;
} }
flex: 0 20%;
display: flex;
flex-direction: row;
margin-left: 5.2rem;
gap: 1rem;
} }
.actionButton { .actionButton {
@ -115,8 +117,4 @@
} }
} }
} }
.actionButtonLabelHovered {
display: none;
}
} }

View File

@ -1,87 +1,80 @@
import { openPage } from '@nanostores/router' import { useNavigate } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js' import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { Button } from '~/components/_shared/Button'
import { useFollowing } from '../../../context/following' import { CheckButton } from '~/components/_shared/CheckButton'
import { useLocalize } from '../../../context/localize' import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { useMediaQuery } from '../../../context/mediaQuery' import { FollowingButton } from '~/components/_shared/FollowingButton'
import { useSession } from '../../../context/session' import { Icon } from '~/components/_shared/Icon'
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen' import { useFollowing } from '~/context/following'
import { router, useRouter } from '../../../stores/router' import { useLocalize } from '~/context/localize'
import { translit } from '../../../utils/ru2en' import { useSession } from '~/context/session'
import { isCyrillic } from '../../../utils/translate' import { Author, FollowingEntity } from '~/graphql/schema/core.gen'
import { Button } from '../../_shared/Button' import { isCyrillic } from '~/intl/translate'
import { CheckButton } from '../../_shared/CheckButton' import { translit } from '~/intl/translit'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { mediaMatches } from '~/lib/mediaQuery'
import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { FollowedInfo } from '../../../pages/types'
import stylesButton from '../../_shared/Button/Button.module.scss'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
type Props = { type Props = {
author: Author author: Author
minimizeSubscribeButton?: boolean minimize?: boolean
showMessageButton?: boolean showMessageButton?: boolean
iconButtons?: boolean iconButtons?: boolean
nameOnly?: boolean nameOnly?: boolean
inviteView?: boolean inviteView?: boolean
onInvite?: (id: number) => void onInvite?: (id: number) => void
selected?: boolean selected?: boolean
isFollowed?: FollowedInfo subscriptionsMode?: boolean
} }
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery() const { session, requireAuthentication } = useSession()
const { author, requireAuthentication } = useSession() const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { follow, unfollow, follows, following } = useFollowing()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>(
Boolean(follows?.authors?.some((authorEntity) => Boolean(authorEntity.id === props.author?.id)))
)
createEffect(() => setIsMobileView(!mediaMatches.sm))
createEffect(
on(
[() => follows?.authors, () => props.author, following],
([followingAuthors, currentAuthor, _]) => {
setIsFollowed(
Boolean(followingAuthors?.some((followedAuthor) => followedAuthor.id === currentAuthor?.id))
)
},
{ defer: true }
)
)
createEffect(() => { const navigate = useNavigate()
setIsMobileView(!mediaMatches.sm)
})
const { setFollowing } = useFollowing()
const { changeSearchParams } = useRouter()
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity // eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, 'inbox') props.author?.id && navigate(`/inbox/${props.author?.id}`, { replace: true })
changeSearchParams({
initChat: props.author.id.toString()
})
}, 'discussions') }, 'discussions')
} }
const name = createMemo(() => { const name = createMemo(() => {
if (lang() !== 'ru' && isCyrillic(props.author.name)) { if (lang() !== 'ru' && isCyrillic(props.author.name || '')) {
if (props.author.name === 'Дискурс') { if (props.author.name === 'Дискурс') {
return 'Discours' return 'Discours'
} }
return translit(props.author.name) return translit(props.author.name || '')
} }
return props.author.name return props.author.name
}) })
createEffect(
on(
() => props.isFollowed,
() => {
setIsFollowed(props.isFollowed.value)
}
)
)
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !isFollowed() requireAuthentication(async () => {
requireAuthentication(() => { const handle = isFollowed() ? unfollow : follow
setIsFollowed(value) await handle(FollowingEntity.Author, props.author.slug)
setFollowing(FollowingEntity.Author, props.author.slug, value) }, 'follow')
}, 'subscribe')
} }
return ( return (
@ -90,14 +83,14 @@ export const AuthorBadge = (props: Props) => {
<Userpic <Userpic
hasLink={true} hasLink={true}
size={isMobileView() ? 'M' : 'L'} size={isMobileView() ? 'M' : 'L'}
name={name()} name={name() || ''}
userpic={props.author.pic} userpic={props.author.pic || ''}
slug={props.author.slug} slug={props.author.slug}
/> />
<ConditionalWrapper <ConditionalWrapper
condition={!props.inviteView} condition={!props.inviteView}
wrapper={(children) => ( wrapper={(children) => (
<a href={`/author/${props.author.slug}`} class={styles.info}> <a href={`/@${props.author.slug}`} class={styles.info}>
{children} {children}
</a> </a>
)} )}
@ -110,22 +103,25 @@ export const AuthorBadge = (props: Props) => {
fallback={ fallback={
<div class={styles.bio}> <div class={styles.bio}>
{t('Registered since {date}', { {t('Registered since {date}', {
date: formatDate(new Date(props.author.created_at * 1000)) date: formatDate(new Date((props.author.created_at || 0) * 1000))
})} })}
</div> </div>
} }
> >
<Match when={props.author.bio}> <Match when={props.author.bio}>
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} /> <div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio || ''} />
</Match> </Match>
</Switch> </Switch>
<Show when={props.author?.stat}> <Show when={props.author?.stat && !props.subscriptionsMode}>
<div class={styles.bio}> <div class={styles.bio}>
<Show when={props.author?.stat.shouts > 0}> <Show when={(props.author?.stat?.shouts || 0) > 0}>
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div> <div>{t('some posts', { count: props.author.stat?.shouts ?? 0 })}</div>
</Show> </Show>
<Show when={props.author?.stat.followers > 0}> <Show when={(props.author?.stat?.comments || 0) > 0}>
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div> <div>{t('some comments', { count: props.author.stat?.comments ?? 0 })}</div>
</Show>
<Show when={(props.author?.stat?.followers || 0) > 0}>
<div>{t('some followers', { count: props.author.stat?.followers ?? 0 })}</div>
</Show> </Show>
</div> </div>
</Show> </Show>
@ -134,55 +130,11 @@ export const AuthorBadge = (props: Props) => {
</div> </div>
<Show when={props.author.slug !== author()?.slug && !props.nameOnly}> <Show when={props.author.slug !== author()?.slug && !props.nameOnly}>
<div class={styles.actions}> <div class={styles.actions}>
<Show <FollowingButton
when={!props.minimizeSubscribeButton} action={handleFollowClick}
fallback={<CheckButton text={t('Follow')} checked={isFollowed()} onClick={handleFollowClick} />} isFollowed={isFollowed()}
> actionMessageType={following()?.slug === props.author.slug ? following()?.type : undefined}
<Show
when={isFollowed()}
fallback={
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show when={props.iconButtons} fallback={t('Subscribe')}>
<Icon name="author-subscribe" class={stylesButton.icon} />
</Show>
}
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: isFollowed()
})}
/> />
}
>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S"
value={
<Show
when={props.iconButtons}
fallback={
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
}
>
<Icon name="author-unsubscribe" class={stylesButton.icon} />
</Show>
}
onClick={handleFollowClick}
isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: isFollowed()
})}
/>
</Show>
</Show>
<Show when={props.showMessageButton}> <Show when={props.showMessageButton}>
<Button <Button
variant={props.iconButtons ? 'secondary' : 'bordered'} variant={props.iconButtons ? 'secondary' : 'bordered'}
@ -197,8 +149,8 @@ export const AuthorBadge = (props: Props) => {
<Show when={props.inviteView}> <Show when={props.inviteView}>
<CheckButton <CheckButton
text={t('Invite')} text={t('Invite')}
checked={props.selected} checked={Boolean(props.selected)}
onClick={() => props.onInvite(props.author.id)} onClick={() => props.onInvite?.(props.author.id)}
/> />
</Show> </Show>
</div> </div>

View File

@ -1,4 +1,16 @@
.author { .author {
@include media-breakpoint-down(md) {
justify-content: center;
}
@include media-breakpoint-up(md) {
margin-bottom: 2.4rem;
}
@include media-breakpoint-down(lg) {
flex-wrap: wrap;
}
display: flex; display: flex;
align-items: center; align-items: center;
flex-flow: row nowrap; flex-flow: row nowrap;
@ -8,19 +20,11 @@
margin-bottom: 0; margin-bottom: 0;
} }
@include media-breakpoint-down(md) {
justify-content: center;
}
@include media-breakpoint-up(md) {
margin-bottom: 2.4rem;
}
.authorName { .authorName {
@include font-size(4rem); @include font-size(4rem);
font-weight: 700; font-weight: 700;
margin-bottom: 0.2em; margin-bottom: 1.2rem;
} }
.authorAbout { .authorAbout {
@ -32,15 +36,15 @@
} }
.authorActions { .authorActions {
@include media-breakpoint-down(md) {
justify-content: center;
}
margin: 2rem -0.8rem 0 0; margin: 2rem -0.8rem 0 0;
padding-left: 0; padding-left: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1rem; gap: 1rem;
@include media-breakpoint-down(md) {
justify-content: center;
}
} }
.authorActionsLabel { .authorActionsLabel {
@ -50,27 +54,24 @@
} }
.authorActionsLabelMobile { .authorActionsLabelMobile {
display: none;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
display: block; display: block;
} }
display: none;
} }
.authorDetails { .authorDetails {
display: block;
margin-bottom: 0;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
flex: 1 100%; flex: 1 100%;
text-align: center; text-align: center;
} }
display: block;
margin-bottom: 0;
} }
.listWrapper & { .listWrapper & {
align-items: flex-start;
margin-bottom: 2rem;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
margin-bottom: 3rem; margin-bottom: 3rem;
} }
@ -79,6 +80,9 @@
margin-bottom: 3rem; margin-bottom: 3rem;
} }
align-items: flex-start;
margin-bottom: 2rem;
.circlewrap { .circlewrap {
margin-top: 1rem; margin-top: 1rem;
} }
@ -88,10 +92,6 @@
} }
} }
@include media-breakpoint-down(lg) {
flex-wrap: wrap;
}
.buttonWriteMessage { .buttonWriteMessage {
border-radius: 0.8rem; border-radius: 0.8rem;
padding-bottom: 0.6rem; padding-bottom: 0.6rem;
@ -100,8 +100,6 @@
} }
.authorDetails { .authorDetails {
flex: 0 0 auto;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
align-items: center; align-items: center;
display: flex; display: flex;
@ -118,12 +116,11 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
flex: 0 0 auto;
} }
.authorDetailsWrapper { .authorDetailsWrapper {
flex: 1 0;
position: relative;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
flex: 1; flex: 1;
} }
@ -139,6 +136,9 @@
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
padding-right: 1.2rem; padding-right: 1.2rem;
} }
flex: 1 0;
position: relative;
} }
.authorName { .authorName {
@ -160,6 +160,15 @@
} }
.authorSubscribeSocial { .authorSubscribeSocial {
@include media-breakpoint-down(sm) {
flex: 1 100%;
justify-content: center;
}
@include media-breakpoint-down(md) {
justify-content: center;
}
align-items: center; align-items: center;
display: flex; display: flex;
margin: 0.5rem 0 2rem -0.4rem; margin: 0.5rem 0 2rem -0.4rem;
@ -175,7 +184,7 @@
width: 24px; width: 24px;
&::before { &::before {
background-image: url(/icons/user-link-default.svg); background-image: url('/icons/user-link-default.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
background-size: contain; background-size: contain;
@ -209,7 +218,7 @@
&[href*='facebook.com/'] { &[href*='facebook.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-facebook.svg); background-image: url('/icons/user-link-facebook.svg');
} }
&:hover { &:hover {
@ -221,7 +230,7 @@
&[href*='twitter.com/'] { &[href*='twitter.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-twitter.svg); background-image: url('/icons/user-link-twitter.svg');
} }
&:hover { &:hover {
@ -234,7 +243,7 @@
&[href*='telegram.com/'], &[href*='telegram.com/'],
&[href*='t.me/'] { &[href*='t.me/'] {
&::before { &::before {
background-image: url(/icons/user-link-telegram.svg); background-image: url('/icons/user-link-telegram.svg');
} }
&:hover { &:hover {
@ -247,7 +256,7 @@
&[href*='vk.cc/'], &[href*='vk.cc/'],
&[href*='vk.com/'] { &[href*='vk.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-vk.svg); background-image: url('/icons/user-link-vk.svg');
} }
&:hover { &:hover {
@ -259,7 +268,7 @@
&[href*='tumblr.com/'] { &[href*='tumblr.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-tumblr.svg); background-image: url('/icons/user-link-tumblr.svg');
} }
&:hover { &:hover {
@ -271,7 +280,7 @@
&[href*='instagram.com/'] { &[href*='instagram.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-instagram.svg); background-image: url('/icons/user-link-instagram.svg');
} }
&:hover { &:hover {
@ -283,7 +292,7 @@
&[href*='behance.net/'] { &[href*='behance.net/'] {
&::before { &::before {
background-image: url(/icons/user-link-behance.svg); background-image: url('/icons/user-link-behance.svg');
} }
&:hover { &:hover {
@ -295,7 +304,7 @@
&[href*='dribbble.com/'] { &[href*='dribbble.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-dribbble.svg); background-image: url('/icons/user-link-dribbble.svg');
} }
&:hover { &:hover {
@ -307,7 +316,7 @@
&[href*='github.com/'] { &[href*='github.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-github.svg); background-image: url('/icons/user-link-github.svg');
} }
&:hover { &:hover {
@ -319,7 +328,7 @@
&[href*='linkedin.com/'] { &[href*='linkedin.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-linkedin.svg); background-image: url('/icons/user-link-linkedin.svg');
} }
&:hover { &:hover {
@ -331,7 +340,7 @@
&[href*='medium.com/'] { &[href*='medium.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-medium.svg); background-image: url('/icons/user-link-medium.svg');
} }
&:hover { &:hover {
@ -343,7 +352,7 @@
&[href*='ok.ru/'] { &[href*='ok.ru/'] {
&::before { &::before {
background-image: url(/icons/user-link-ok.svg); background-image: url('/icons/user-link-ok.svg');
} }
&:hover { &:hover {
@ -355,7 +364,7 @@
&[href*='pinterest.com/'] { &[href*='pinterest.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-pinterest.svg); background-image: url('/icons/user-link-pinterest.svg');
} }
&:hover { &:hover {
@ -367,7 +376,7 @@
&[href*='reddit.com/'] { &[href*='reddit.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-reddit.svg); background-image: url('/icons/user-link-reddit.svg');
} }
&:hover { &:hover {
@ -379,7 +388,7 @@
&[href*='tiktok.com/'] { &[href*='tiktok.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-tiktok.svg); background-image: url('/icons/user-link-tiktok.svg');
} }
&:hover { &:hover {
@ -392,7 +401,7 @@
&[href*='youtube.com/'], &[href*='youtube.com/'],
&[href*='youtu.be/'] { &[href*='youtu.be/'] {
&::before { &::before {
background-image: url(/icons/user-link-youtube.svg); background-image: url('/icons/user-link-youtube.svg');
} }
&:hover { &:hover {
@ -404,7 +413,7 @@
&[href*='dzen.ru/'] { &[href*='dzen.ru/'] {
&::before { &::before {
background-image: url(/icons/user-link-dzen.svg); background-image: url('/icons/user-link-dzen.svg');
} }
&:hover { &:hover {
@ -415,78 +424,24 @@
} }
} }
@include media-breakpoint-down(sm) {
flex: 1 100%;
justify-content: center;
}
@include media-breakpoint-down(md) {
justify-content: center;
}
a:link { a:link {
border: none; border: none;
} }
} }
.subscribersContainer {
display: flex;
flex-wrap: wrap;
font-size: 1.4rem;
margin-top: 1.5rem;
@include media-breakpoint-down(md) {
justify-content: center;
}
}
.subscribers {
align-items: center;
cursor: pointer;
display: inline-flex;
margin: 0 2% 1rem;
vertical-align: top;
border-bottom: unset !important;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
.subscribersItem {
position: relative;
&:nth-child(1) {
z-index: 2;
}
&:nth-child(2) {
z-index: 1;
}
&:not(:last-child) {
margin-right: -4px;
box-shadow: 0 0 0 1px var(--background-color);
}
}
.subscribersCounter {
font-weight: 500;
margin-left: 1rem;
}
&:hover {
background: none !important;
.subscribersCounter {
background: var(--background-color-invert);
}
}
}
.listWrapper { .listWrapper {
max-height: 70vh; max-height: 70vh;
} }
.subscribersContainer {
@include media-breakpoint-down(md) {
justify-content: center;
}
display: flex;
flex-wrap: wrap;
font-size: 1.4rem;
gap: 1rem;
margin-top: 0;
white-space: nowrap;
}

View File

@ -1,97 +1,88 @@
import type { Author, Community } from '../../../graphql/schema/core.gen' import { redirect, useNavigate } from '@solidjs/router'
import { openPage, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { Button } from '~/components/_shared/Button'
import { useFollowing } from '../../../context/following' import stylesButton from '~/components/_shared/Button/Button.module.scss'
import { useLocalize } from '../../../context/localize' import { FollowingCounters } from '~/components/_shared/FollowingCounters/FollowingCounters'
import { useSession } from '../../../context/session' import { ShowOnlyOnClient } from '~/components/_shared/ShowOnlyOnClient'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen' import { FollowsFilter, useFollowing } from '~/context/following'
import { SubscriptionFilter } from '../../../pages/types' import { useLocalize } from '~/context/localize'
import { router, useRouter } from '../../../stores/router' import { useSession } from '~/context/session'
import { isAuthor } from '../../../utils/isAuthor' import type { Author, Community } from '~/graphql/schema/core.gen'
import { translit } from '../../../utils/ru2en' import { FollowingEntity, Topic } from '~/graphql/schema/core.gen'
import { isCyrillic } from '../../../utils/translate' import { isCyrillic } from '~/intl/translate'
import { translit } from '~/intl/translit'
import { SharePopup, getShareUrl } from '../../Article/SharePopup' import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { Modal } from '../../Nav/Modal'
import { TopicBadge } from '../../Topic/TopicBadge' import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button' import { Modal } from '../../_shared/Modal'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { AuthorBadge } from '../AuthorBadge' import { AuthorBadge } from '../AuthorBadge'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import stylesButton from '../../_shared/Button/Button.module.scss'
import styles from './AuthorCard.module.scss' import styles from './AuthorCard.module.scss'
type Props = { type Props = {
author: Author author: Author
followers?: Author[] followers?: Author[]
following?: Array<Author | Topic> flatFollows?: Array<Author | Topic>
} }
export const AuthorCard = (props: Props) => { export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { author, isSessionLoaded, requireAuthentication } = useSession() const navigate = useNavigate()
const { session, isSessionLoaded, requireAuthentication } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([]) const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [followsFilter, setFollowsFilter] = createSignal<FollowsFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
const isProfileOwner = createMemo(() => author()?.slug === props.author.slug) const isProfileOwner = createMemo(() => author()?.slug === props.author.slug)
const { setFollowing, isOwnerSubscribed } = useFollowing() const { follow, unfollow, follows, following } = useFollowing() // viewer's followings
onMount(() => {
setAuthorSubs(props.following)
})
createEffect(() => { createEffect(() => {
setIsFollowed(isOwnerSubscribed(props.author?.id)) if (!(follows && props.author)) return
const followed = follows?.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsFollowed(followed)
}) })
const name = createMemo(() => { const name = createMemo(() => {
if (lang() !== 'ru' && isCyrillic(props.author.name)) { if (lang() !== 'ru' && isCyrillic(props.author?.name || '')) {
if (props.author.name === 'Дискурс') { if (props.author.name === 'Дискурс') {
return 'Discours' return 'Discours'
} }
return translit(props.author.name) return translit(props.author?.name || '')
} }
return props.author.name return props.author.name
}) })
// TODO: reimplement AuthorCard
const { changeSearchParams } = useRouter()
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity // eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, 'inbox') props.author?.id && navigate(`/inbox/${props.author?.id}`, { replace: true })
changeSearchParams({
initChat: props.author.id.toString()
})
}, 'discussions') }, 'discussions')
} }
createEffect(() => { createEffect(
if (props.following) { on(followsFilter, (f = 'all') => {
if (subscriptionFilter() === 'authors') { const subs =
setAuthorSubs(props.following.filter((s) => 'name' in s)) f !== 'all'
} else if (subscriptionFilter() === 'topics') { ? follows[f as keyof typeof follows]
setAuthorSubs(props.following.filter((s) => 'title' in s)) : [...(follows.topics || []), ...(follows.authors || [])]
} else if (subscriptionFilter() === 'communities') { setAuthorSubs(subs || [])
setAuthorSubs(props.following.filter((s) => 'title' in s))
} else {
setAuthorSubs(props.following)
}
}
}) })
)
const handleFollowClick = () => { const handleFollowClick = () => {
const value = !isFollowed()
requireAuthentication(() => { requireAuthentication(() => {
setIsFollowed(value) isFollowed()
setFollowing(FollowingEntity.Author, props.author.slug, value) ? unfollow(FollowingEntity.Author, props.author.slug)
}, 'subscribe') : follow(FollowingEntity.Author, props.author.slug)
}, 'follow')
} }
const followButtonText = createMemo(() => { const followButtonText = createMemo(() => {
if (isOwnerSubscribed(props.author?.id)) { if (following()?.slug === props.author.slug) {
return following()?.type === 'follow' ? t('Following...') : t('Unfollowing...')
}
if (isFollowed()) {
return ( return (
<> <>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span> <span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
@ -102,13 +93,82 @@ export const AuthorCard = (props: Props) => {
return t('Follow') return t('Follow')
}) })
const FollowersModalView = () => (
<>
<h2>{t('Followers')}</h2>
<div class={styles.listWrapper}>
<div class="row">
<div class="col-24">
<For each={props.followers}>{(follower: Author) => <AuthorBadge author={follower} />}</For>
</div>
</div>
</div>
</>
)
const FollowingModalView = () => (
<>
<h2>{t('Subscriptions')}</h2>
<ul class="view-switcher">
<li
class={clsx({
'view-switcher__item--selected': followsFilter() === 'all'
})}
>
<button type="button" onClick={() => setFollowsFilter('all')}>
{t('All')}
</button>
<span class="view-switcher__counter">{props.flatFollows?.length}</span>
</li>
<li
class={clsx({
'view-switcher__item--selected': followsFilter() === 'authors'
})}
>
<button type="button" onClick={() => setFollowsFilter('authors')}>
{t('Authors')}
</button>
<span class="view-switcher__counter">{props.flatFollows?.filter((s) => 'name' in s).length}</span>
</li>
<li
class={clsx({
'view-switcher__item--selected': followsFilter() === 'topics'
})}
>
<button type="button" onClick={() => setFollowsFilter('topics')}>
{t('Topics')}
</button>
<span class="view-switcher__counter">
{props.flatFollows?.filter((s) => 'title' in s).length}
</span>
</li>
</ul>
<br />
<div class={styles.listWrapper}>
<div class="row">
<div class="col-24">
<For each={authorSubs()}>
{(subscription) =>
'name' in subscription ? (
<AuthorBadge author={subscription as Author} subscriptionsMode={true} />
) : (
<TopicBadge topic={subscription as Topic} subscriptionsMode={true} />
)
}
</For>
</div>
</div>
</div>
</>
)
return ( return (
<div class={clsx(styles.author, 'row')}> <div class={clsx(styles.author, 'row')}>
<div class="col-md-5"> <div class="col-md-5">
<Userpic <Userpic
size={'XL'} size={'XL'}
name={props.author.name} name={props.author.name || ''}
userpic={props.author.pic} userpic={props.author.pic || ''}
slug={props.author.slug} slug={props.author.slug}
class={styles.circlewrap} class={styles.circlewrap}
/> />
@ -117,62 +177,16 @@ export const AuthorCard = (props: Props) => {
<div class={styles.authorDetailsWrapper}> <div class={styles.authorDetailsWrapper}>
<div class={styles.authorName}>{name()}</div> <div class={styles.authorName}>{name()}</div>
<Show when={props.author.bio}> <Show when={props.author.bio}>
<div class={styles.authorAbout} innerHTML={props.author.bio} /> <div class={styles.authorAbout} innerHTML={props.author.bio || ''} />
</Show> </Show>
<Show <Show when={(props.followers || [])?.length > 0 || (props.flatFollows || []).length > 0}>
when={
(props.followers && props.followers.length > 0) ||
(props.following && props.following.length > 0)
}
>
<div class={styles.subscribersContainer}> <div class={styles.subscribersContainer}>
<Show when={props.followers && props.followers.length > 0}> <FollowingCounters
<a href="?m=followers" class={styles.subscribers}> followers={props.followers}
<For each={props.followers.slice(0, 3)}> followersAmount={props.author?.stat?.followers || 0}
{(f) => ( following={props.flatFollows}
<Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} /> followingAmount={props.flatFollows?.length || 0}
)}
</For>
<div class={styles.subscribersCounter}>
{t('SubscriberWithCount', { count: props.followers.length ?? 0 })}
</div>
</a>
</Show>
<Show when={props.following && props.following.length > 0}>
<a href="?m=following" class={styles.subscribers}>
<For each={props.following.slice(0, 3)}>
{(f) => {
if ('name' in f) {
return (
<Userpic
size={'XS'}
name={f.name}
userpic={f.pic}
class={styles.subscribersItem}
/> />
)
}
if ('title' in f) {
return (
<Userpic
size={'XS'}
name={f.title}
userpic={f.pic}
class={styles.subscribersItem}
/>
)
}
return null
}}
</For>
<div class={styles.subscribersCounter}>
{t('SubscriptionWithCount', { count: props?.following.length ?? 0 })}
</div>
</a>
</Show>
</div> </div>
</Show> </Show>
</div> </div>
@ -181,15 +195,15 @@ export const AuthorCard = (props: Props) => {
<Show when={props.author.links && props.author.links.length > 0}> <Show when={props.author.links && props.author.links.length > 0}>
<div class={styles.authorSubscribeSocial}> <div class={styles.authorSubscribeSocial}>
<For each={props.author.links}> <For each={props.author.links}>
{(link) => ( {(link: string | null) => (
<a <a
class={styles.socialLink} class={styles.socialLink}
href={link.startsWith('http') ? link : `https://${link}`} href={link?.startsWith('http') ? link : `https://${link}`}
target="_blank" target="_blank"
rel="nofollow noopener noreferrer" rel="nofollow noopener noreferrer"
> >
<span class={styles.authorSubscribeSocialLabel}> <span class={styles.authorSubscribeSocialLabel}>
{link.startsWith('http') ? link : `https://${link}`} {link?.startsWith('http') ? link : `https://${link}`}
</span> </span>
</a> </a>
)} )}
@ -200,13 +214,14 @@ export const AuthorCard = (props: Props) => {
when={isProfileOwner()} when={isProfileOwner()}
fallback={ fallback={
<div class={styles.authorActions}> <div class={styles.authorActions}>
<Show when={authorSubs().length}> <Show when={authorSubs()?.length}>
<Button <Button
onClick={handleFollowClick} onClick={handleFollowClick}
disabled={Boolean(following())}
value={followButtonText()} value={followButtonText()}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx({ class={clsx({
[stylesButton.subscribed]: isFollowed() [stylesButton.followed]: isFollowed()
})} })}
/> />
</Show> </Show>
@ -222,7 +237,7 @@ export const AuthorCard = (props: Props) => {
<div class={styles.authorActions}> <div class={styles.authorActions}>
<Button <Button
variant="secondary" variant="secondary"
onClick={() => redirectPage(router, 'profileSettings')} onClick={() => redirect('/settings')}
value={ value={
<> <>
<span class={styles.authorActionsLabel}>{t('Edit profile')}</span> <span class={styles.authorActionsLabel}>{t('Edit profile')}</span>
@ -231,10 +246,12 @@ export const AuthorCard = (props: Props) => {
} }
/> />
<SharePopup <SharePopup
title={props.author.name} title={props.author.name || ''}
description={props.author.bio} description={props.author.bio || ''}
imageUrl={props.author.pic} imageUrl={props.author.pic || ''}
shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })} shareUrl={getShareUrl({
pathname: `/@${props.author.slug}`
})}
trigger={<Button variant="secondary" value={t('Share')} />} trigger={<Button variant="secondary" value={t('Share')} />}
/> />
</div> </div>
@ -243,85 +260,12 @@ export const AuthorCard = (props: Props) => {
</ShowOnlyOnClient> </ShowOnlyOnClient>
<Show when={props.followers}> <Show when={props.followers}>
<Modal variant="medium" isResponsive={true} name="followers" maxHeight> <Modal variant="medium" isResponsive={true} name="followers" maxHeight>
<> <FollowersModalView />
<h2>{t('Followers')}</h2>
<div class={styles.listWrapper}>
<div class="row">
<div class="col-24">
<For each={props.followers}>
{(follower: Author) => (
<AuthorBadge
author={follower}
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(follower.id)
}}
/>
)}
</For>
</div>
</div>
</div>
</>
</Modal> </Modal>
</Show> </Show>
<Show when={props.following}> <Show when={props.flatFollows}>
<Modal variant="medium" isResponsive={true} name="following" maxHeight> <Modal variant="medium" isResponsive={true} name="following" maxHeight>
<> <FollowingModalView />
<h2>{t('Subscriptions')}</h2>
<ul class="view-switcher">
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
<button type="button" onClick={() => setSubscriptionFilter('all')}>
{t('All')}
</button>
<span class="view-switcher__counter">{props.following.length}</span>
</li>
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'authors' })}>
<button type="button" onClick={() => setSubscriptionFilter('authors')}>
{t('Authors')}
</button>
<span class="view-switcher__counter">
{props.following.filter((s) => 'name' in s).length}
</span>
</li>
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
{t('Topics')}
</button>
<span class="view-switcher__counter">
{props.following.filter((s) => 'title' in s).length}
</span>
</li>
</ul>
<br />
<div class={styles.listWrapper}>
<div class="row">
<div class="col-24">
<For each={authorSubs()}>
{(subscription) =>
isAuthor(subscription) ? (
<AuthorBadge
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id)
}}
author={subscription}
/>
) : (
<TopicBadge
isFollowed={{
loaded: Boolean(authorSubs()),
value: isOwnerSubscribed(subscription.id)
}}
topic={subscription}
/>
)
}
</For>
</div>
</div>
</div>
</>
</Modal> </Modal>
</Show> </Show>
</div> </div>

View File

@ -1,14 +1,14 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '~/context/localize'
import { Author } from '../../../graphql/schema/core.gen' import { Author } from '~/graphql/schema/core.gen'
import { capitalize } from '../../../utils/capitalize' import { isCyrillic } from '~/intl/translate'
import { translit } from '../../../utils/ru2en' import { translit } from '~/intl/translit'
import { isCyrillic } from '../../../utils/translate' import { capitalize } from '~/utils/capitalize'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AhtorLink.module.scss' import styles from './AuthorLink.module.scss'
type Props = { type Props = {
author: Author author: Author
@ -20,18 +20,18 @@ type Props = {
export const AuthorLink = (props: Props) => { export const AuthorLink = (props: Props) => {
const { lang } = useLocalize() const { lang } = useLocalize()
const name = createMemo(() => { const name = createMemo(() => {
return lang() === 'en' && isCyrillic(props.author.name) return lang() === 'en' && isCyrillic(props.author.name || '')
? translit(capitalize(props.author.name)) ? translit(capitalize(props.author.name || ''))
: props.author.name : props.author.name
}) })
return ( return (
<div <div
class={clsx(styles.AuthorLink, props.class, styles[props.size ?? 'M'], { class={clsx(styles.AuthorLink, props.class, styles[(props.size ?? 'M') as keyof Props['size']], {
[styles.authorLinkFloorImportant]: props.isFloorImportant [styles.authorLinkFloorImportant]: props.isFloorImportant
})} })}
> >
<a class={styles.link} href={`/author/${props.author.slug}`}> <a class={styles.link} href={`/@${props.author.slug}`}>
<Userpic size={props.size ?? 'M'} name={name()} userpic={props.author.pic} /> <Userpic size={props.size ?? 'M'} name={name() || ''} userpic={props.author.pic || ''} />
<div class={styles.name}>{name()}</div> <div class={styles.name}>{name()}</div>
</a> </a>
</div> </div>

View File

@ -1,10 +1,9 @@
import type { Author } from '../../graphql/schema/core.gen' import type { Author } from '~/graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useSession } from '~/context/session'
import { apiClient } from '../../graphql/client/core' import rateAuthorMutation from '~/graphql/mutation/core/author-rate'
import styles from './AuthorRatingControl.module.scss' import styles from './AuthorRatingControl.module.scss'
interface AuthorRatingControlProps { interface AuthorRatingControlProps {
@ -15,16 +14,23 @@ interface AuthorRatingControlProps {
export const AuthorRatingControl = (props: AuthorRatingControlProps) => { export const AuthorRatingControl = (props: AuthorRatingControlProps) => {
const isUpvoted = false const isUpvoted = false
const isDownvoted = false const isDownvoted = false
const { client } = useSession()
// eslint-disable-next-line unicorn/consistent-function-scoping // eslint-disable-next-line unicorn/consistent-function-scoping
const handleRatingChange = async (isUpvote: boolean) => { const handleRatingChange = async (isUpvote: boolean) => {
console.log('handleRatingChange', { isUpvote }) console.log('handleRatingChange', { isUpvote })
if (props.author?.slug) { if (props.author?.slug) {
const value = isUpvote ? 1 : -1 const value = isUpvote ? 1 : -1
await apiClient.rateAuthor({ rated_slug: props.author?.slug, value }) const _resp = await client()
setRating((r) => r + value) ?.mutation(rateAuthorMutation, {
rated_slug: props.author?.slug,
value
})
.toPromise()
setRating((r) => (r || 0) + value)
} }
} }
const [rating, setRating] = createSignal(props.author?.stat?.rating) const [rating, setRating] = createSignal(props.author?.stat?.rating)
return ( return (
<div <div

View File

@ -1,4 +1,4 @@
import type { Author } from '../../graphql/schema/core.gen' import type { Author } from '~/graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
@ -11,7 +11,7 @@ interface AuthorShoutsRating {
} }
export const AuthorShoutsRating = (props: AuthorShoutsRating) => { export const AuthorShoutsRating = (props: AuthorShoutsRating) => {
const isUpvoted = createMemo(() => props.author?.stat?.rating_shouts > 0) const isUpvoted = createMemo(() => (props.author?.stat?.rating_shouts || 0) > 0)
return ( return (
<div <div
class={clsx(styles.rating, props.class, { class={clsx(styles.rating, props.class, {

View File

@ -86,17 +86,17 @@
} }
&.XL { &.XL {
@include media-breakpoint-up(md) {
margin: 0;
max-width: 100%;
}
aspect-ratio: 1/1; aspect-ratio: 1/1;
margin: 0 auto 1rem; margin: 0 auto 1rem;
max-width: 168px; max-width: 168px;
height: auto; height: auto;
width: 100%; width: 100%;
@include media-breakpoint-up(md) {
margin: 0;
max-width: 100%;
}
.letters { .letters {
align-items: center; align-items: center;
display: flex; display: flex;

View File

@ -1,9 +1,9 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo } from 'solid-js' import { Show, createMemo } from 'solid-js'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { Image } from '../../_shared/Image' import { Image } from '~/components/_shared/Image'
import { Loading } from '../../_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import styles from './Userpic.module.scss' import styles from './Userpic.module.scss'
@ -22,7 +22,7 @@ export const Userpic = (props: Props) => {
const letters = () => { const letters = () => {
if (!props.name) return if (!props.name) return
const names = props.name ? props.name.split(' ') : [] const names = props.name ? props.name.split(' ') : []
return `${names[0][0 ?? names[0][0]]}.${names.length > 1 ? `${names[1][0]}.` : ''}` return `${names[0][0] ? names[0][0] : ''}.${names.length > 1 ? `${names[1][0]}.` : ''}`
} }
const avatarSize = createMemo(() => { const avatarSize = createMemo(() => {
@ -54,8 +54,8 @@ export const Userpic = (props: Props) => {
> >
<Show when={!props.loading} fallback={<Loading />}> <Show when={!props.loading} fallback={<Loading />}>
<ConditionalWrapper <ConditionalWrapper
condition={props.hasLink} condition={Boolean(props.hasLink)}
wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>} wrapper={(children) => <a href={`/@${props.slug}`}>{children}</a>}
> >
<Show keyed={true} when={props.userpic} fallback={<div class={styles.letters}>{letters()}</div>}> <Show keyed={true} when={props.userpic} fallback={<div class={styles.letters}>{letters()}</div>}>
<Image src={props.userpic} width={avatarSize()} height={avatarSize()} alt={props.name} /> <Image src={props.userpic} width={avatarSize()} height={avatarSize()} alt={props.name} />

View File

@ -1,26 +0,0 @@
.AuthorsList {
.action {
display: flex;
align-items: center;
justify-content: center;
min-height: 8rem;
}
.loading {
@include font-size(1.4rem);
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
width: 100%;
flex-direction: row;
opacity: 0.5;
.icon {
position: relative;
width: 18px;
height: 18px;
}
}
}

View File

@ -1,90 +0,0 @@
import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize'
import { apiClient } from '../../graphql/client/core'
import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors'
import { AuthorBadge } from '../Author/AuthorBadge'
import { InlineLoader } from '../InlineLoader'
import { Button } from '../_shared/Button'
import styles from './AuthorsList.module.scss'
type Props = {
class?: string
query: 'shouts' | 'followers'
}
const PAGE_SIZE = 20
export const AuthorsList = (props: Props) => {
const { t } = useLocalize()
const { isOwnerSubscribed } = useFollowing()
const [loading, setLoading] = createSignal(false)
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
const { authorsByShouts, authorsByFollowers } = useAuthorsStore()
const fetchAuthors = async (queryType: 'shouts' | 'followers', page: number) => {
setLoading(true)
const offset = PAGE_SIZE * page
const result = await apiClient.loadAuthorsBy({
by: { order: queryType },
limit: PAGE_SIZE,
offset: offset
})
if (queryType === 'shouts') {
setAuthorsByShouts((prev) => [...prev, ...result])
} else {
setAuthorsByFollowers((prev) => [...prev, ...result])
}
setLoading(false)
return result
}
const loadMoreAuthors = () => {
const queryType = props.query
const nextPage = currentPage()[queryType] + 1
fetchAuthors(queryType, nextPage).then(() =>
setCurrentPage({ ...currentPage(), [queryType]: nextPage })
)
}
createEffect(() => {
const queryType = props.query
if (
currentPage()[queryType] === 0 &&
(authorsByShouts().length === 0 || authorsByFollowers().length === 0)
) {
loadMoreAuthors()
}
})
const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers())
return (
<div class={clsx(styles.AuthorsList, props.class)}>
<For each={authorsList()}>
{(author) => (
<div class="row">
<div class="col-lg-20 col-xl-18">
<AuthorBadge
author={author}
isFollowed={{
loaded: !loading(),
value: isOwnerSubscribed(author.id)
}}
/>
</div>
</div>
)}
</For>
<div class={styles.action}>
<Show when={!loading()}>
<Button value={t('Load more')} onClick={loadMoreAuthors} />
</Show>
<Show when={loading()}>
<InlineLoader />
</Show>
</div>
</div>
)
}

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