271 Commits

Author SHA1 Message Date
Davor Hrg
143371166a step and decimal calculation proposal 2021-08-21 17:57:51 +02:00
Kurt Hutten
14600d3899 live customizer attempt 2021-08-21 11:07:09 +10:00
Kurt Hutten
b0647171d8 r3f-ify jsCadController 2021-08-21 11:04:01 +10:00
Kurt Hutten
ac233a5920 Merge pull request #461 from Irev-Dev/kurt/cadhub-customizer-test-code-437
CadHub Customizer
2021-08-21 11:02:56 +10:00
Kurt Hutten
0cc335ea9b Add demo for cadhub customizer
relates to #437
2021-08-21 11:02:18 +10:00
Kurt Hutten
1336ffc437 Fix editing project without social image. 2021-08-21 08:44:51 +10:00
Kurt Hutten
9186e457d9 Fix JSCAD, useEdgeSplit issue 2021-08-18 05:18:46 +10:00
Kurt Hutten
8dbb0468f8 Tweak SEO copy 2021-08-17 19:47:19 +10:00
Kurt Hutten
246a677517 Merge pull request #460 from Irev-Dev/kurt/459
EdgeSplit not flushing out old geometry
2021-08-17 05:14:57 +10:00
Kurt Hutten
644ef8d189 EdgeSplit not flushing out old geometry
Mostly effecting CQ, changing the script and updating leaves a ghost of
the old geometry in the viewer

Resolves #459
2021-08-17 05:13:30 +10:00
Kurt Hutten
daa0d788af Merge pull request #458 from Irev-Dev/kurt/97-clean-up
social card clean up
2021-08-17 05:10:26 +10:00
Kurt Hutten
93a1c7a242 Invalidate social card on title or description change 2021-08-17 05:05:13 +10:00
Kurt Hutten
31583104bd Stop SEO from overrideding preiveous meta tags with falsy content 2021-08-17 04:32:36 +10:00
Kurt Hutten
bb8a3f4dfc Update popup layout 2021-08-17 03:55:38 +10:00
Kurt Hutten
150ab45748 minimize social cards sdl 2021-08-17 03:35:15 +10:00
Kurt Hutten
b902713847 remove ".public_id" since we're not uploading directly to cloudinary 2021-08-16 20:33:31 +10:00
Kurt Hutten
fd2fc92b72 Merge pull request #457 from Irev-Dev/kurt/454
Make sure bio updates after saving the profile.
2021-08-16 20:32:40 +10:00
Kurt Hutten
0d6c8aa261 Make sure bio updates after saving the profile.
resolves #454
2021-08-16 20:11:30 +10:00
Kurt Hutten
580dbb88e6 Some clean up 2021-08-16 20:06:17 +10:00
Kurt Hutten
9fa22a0469 got something working thats only a little hacky 2021-08-16 18:45:33 +10:00
Kurt Hutten
180cbb9503 format project 2021-08-14 21:03:05 +10:00
Kurt Hutten
567da606f3 Bump SocialCard font sizes 2021-08-14 18:16:28 +10:00
Kurt Hutten
182ec78f79 Fix prerender 2021-08-14 16:04:49 +10:00
Kurt Hutten
4cfacb2581 Merge pull request #452 from Irev-Dev/fis-jscad-params-text
parse as number even for text if numeric string
2021-08-14 15:51:53 +10:00
Kurt Hutten
badcb96c6b Merge pull request #453 from Irev-Dev/kurt/social-card-new-approach-97
social card new approach 97
2021-08-14 15:50:24 +10:00
Kurt Hutten
224eb1d3ba Give some TLC the meta tags 2021-08-14 15:49:35 +10:00
Kurt Hutten
709c653afb add card to ide too 2021-08-14 15:31:04 +10:00
Kurt Hutten
32155ba98c Get image upload to cloudinary with the same public id
This means we can put a consistent url in the head for the card image
2021-08-14 15:19:48 +10:00
Davor Hrg
075779f107 parse as number even for text if numeric string 2021-08-13 22:43:45 +02:00
Kurt Hutten
50e9ac61f8 formatting and helpers 2021-08-13 06:38:15 +10:00
Kurt Hutten
7bd3cb44f8 Fix typo 2021-08-12 07:07:56 +10:00
Kurt Hutten
876dc94cc9 Merge branch 'main' of github.com:Irev-Dev/cadhub 2021-08-12 06:54:20 +10:00
Kurt Hutten
74677c89a2 Add more scene tweaks 2021-08-12 06:49:30 +10:00
Kurt Hutten
484c50c921 Adjust scene lighting, attach light to camera for consistent lighting 2021-08-12 06:15:23 +10:00
Kurt Hutten
0ae5065aaf Add welcome message 2021-08-12 06:14:40 +10:00
Kurt Hutten
5677855a43 Merge pull request #449 from Irev-Dev/kurt/safari-flex-gap-435
Fix safari flexbox/gap issue with grid
2021-08-12 05:38:12 +10:00
Kurt Hutten
0376d48bb9 Merge pull request #451 from Irev-Dev/franknoirot/435
Fix safari z-index issue with MaterialUI model style tweak.
2021-08-12 05:14:10 +10:00
Kurt Hutten
63ce7e9fa0 Merge pull request #450 from Irev-Dev/kurt/434-rename-projects-safari
Fix project renaming in safari
2021-08-12 05:12:50 +10:00
Kurt Hutten
d89e080b36 Fix project renaming in safari
Resolves #434
2021-08-11 20:43:19 +10:00
Kurt Hutten
000bf4c027 fix hight issue on details page 2021-08-11 20:17:36 +10:00
Kurt Hutten
1d1f62e38e Fix safari flexbox/gap issue with grid
related to #435
2021-08-11 19:27:08 +10:00
Kurt Hutten
b255af5f0f Merge pull request #448 from Irev-Dev/kurt/383-add-gizmo-and-edges
Add gizmo and edges
2021-08-11 07:05:01 +10:00
Kurt Hutten
799a32544b Use forked useEdegeSplit to allow updating geomerty 2021-08-11 07:01:09 +10:00
Kurt Hutten
5afa5d953c Add edges to models 2021-08-10 20:36:30 +10:00
Kurt Hutten
d2ee8be7e1 some progress on getting the gizmo work but with jank. 2021-08-10 17:37:28 +10:00
Frank Noirot
68346e075a fixed safari z-index issue with MaterialUI model style tweak. 2021-08-10 00:24:02 -04:00
Kurt Hutten
b5470c873b Merge pull request #446 from Irev-Dev/kurt/make-z-axis-up-444
make z axis up and other viewer improvements
2021-08-10 07:31:07 +10:00
Kurt Hutten
81cdeea761 Fix split edge issue 2021-08-10 06:40:27 +10:00
Kurt Hutten
3d02143422 Bunch of teaks to IdeViewer
- add grid
- slit normals
- resync openscad camera with three camera
- fix stl download for CQ and openscad
- clean up failed preview image attempt
2021-08-09 18:12:14 +10:00
Kurt Hutten
283bdcc56d Merge remote-tracking branch 'origin/main' into kurt/make-z-axis-up-444 2021-08-09 09:20:44 +10:00
Kurt Hutten
01977170f1 Merge pull request #445 from Irev-Dev/fix-default-jscad-script
update default script to be smaller
2021-08-08 20:14:01 +10:00
Kurt Hutten
b88ace2117 Change Z axis to up for Three.json
Openscad camera still needs to be fixed
2021-08-08 19:56:53 +10:00
Kurt Hutten
078c79d8ee format project 2021-08-08 19:55:05 +10:00
Davor Hrg
696441c39b Update useIdeState.ts 2021-08-08 11:52:35 +02:00
Kurt Hutten
802ea61639 Merge pull request #440 from Irev-Dev/kurt/customizer-start-320
jscad customizer with non-react component
2021-08-08 07:09:27 +10:00
Kurt Hutten
2ea15df9d1 defer ping animation 2021-08-08 07:06:20 +10:00
Davor Hrg
ff492fc1c7 Update jscadParams.ts 2021-08-07 16:03:59 +02:00
Kurt Hutten
dcd9d42d32 deeply compare old and new params 2021-08-07 21:26:11 +10:00
Davor Hrg
454995304a don't allow group collapse/exp to triger render, only store values 2021-08-07 11:32:03 +02:00
Kurt Hutten
8dd8e2e749 Tweak customizer styles and hide on non-jscad screens 2021-08-07 19:24:49 +10:00
Kurt Hutten
02160e1e8e store latest params in the store 2021-08-07 17:09:56 +10:00
Kurt Hutten
2d7df96ad9 Add a few type improvements to jscadParams.ts 2021-08-07 15:49:53 +10:00
Kurt Hutten
f83d1b395f Fix param panel collapse issue 2021-08-07 13:53:17 +10:00
Kurt Hutten
53da1e49a0 Remove console logs 2021-08-06 20:26:21 +10:00
Kurt Hutten
7cc989014a format project 2021-08-06 20:17:55 +10:00
Davor Hrg
725e877f1b Update useIdeState.ts 2021-08-06 11:26:44 +02:00
Davor Hrg
e856f8208e Update useIdeState.ts 2021-08-06 11:26:00 +02:00
Davor Hrg
e851593c12 cleanup, few fixes 2021-08-06 11:22:09 +02:00
Davor Hrg
80b12a6e3a Merge branch 'kurt/customizer-start-320' of https://github.com/Irev-Dev/cadhub into kurt/customizer-start-320 2021-08-04 23:59:42 +02:00
Davor Hrg
00c0ae801a ... 2021-08-04 23:54:45 +02:00
Davor Hrg
9dca2eb1c1 Merge pull request #438 from Irev-Dev/kurt/checkbox-320
Add live update checkbox to customizer
2021-08-04 23:54:32 +02:00
Kurt Hutten
c9e3d3a708 Add live update checkbox to customizer 2021-08-05 07:44:21 +10:00
Davor Hrg
2b2080c232 more param types to check and style 2021-08-04 00:11:55 +02:00
Davor Hrg
fd7954815a Merge branch 'main' into kurt/customizer-start-320 2021-08-03 21:34:56 +02:00
Kurt Hutten
879e63157d Merge pull request #433 from revolter/patch-2
Fix incorrect docs and blog edit links
2021-08-04 05:26:15 +10:00
Kurt Hutten
d8c80928b0 Merge pull request #432 from revolter/patch-1
Fix typo in docs page
2021-08-04 05:24:55 +10:00
Iulian Onofrei
b6cb22ed2d Fix incorrect docs and blog edit links 2021-08-03 16:58:34 +03:00
Iulian Onofrei
a06e71291f Fix typo in docs page 2021-08-03 16:53:27 +03:00
Davor Hrg
77ee959c43 working somewhat ok 2021-08-02 23:24:26 +02:00
Kurt Hutten
cd9a4794ac Merge pull request #431 from Irev-Dev/fix-stl-export
fix stl export for jscad
2021-08-03 05:15:37 +10:00
Kurt Hutten
336501aaff add customizerParams to state 2021-08-02 23:19:57 +10:00
Kurt Hutten
8883df3445 Add the beginnings of the customizer UI
Relates to #320
2021-08-02 22:48:59 +10:00
Davor Hrg
7178313c2e Update helpers.ts 2021-08-02 13:39:18 +02:00
Kurt Hutten
5e14ad0829 fix constructor bug 2021-08-01 21:13:19 +10:00
Kurt Hutten
39cbdc749b get deployments working again 2021-08-01 20:39:26 +10:00
Kurt Hutten
cedad90513 update import style 2021-08-01 19:53:05 +10:00
Kurt Hutten
7f29fd5125 Merge pull request #428 from Irev-Dev/kurt/411-demo-branch
initial jscad integration
2021-08-01 19:35:56 +10:00
Davor Hrg
67d5d76d67 Delete CSGToBuffers.js 2021-08-01 10:47:58 +02:00
Kurt Hutten
43477d33cc useEntities directly 2021-08-01 17:15:07 +10:00
Kurt Hutten
d8998a73b3 format project 2021-08-01 17:09:47 +10:00
Kurt Hutten
625db5e26b Merge branch 'main' into kurt/411-demo-branch 2021-08-01 09:46:41 +10:00
Kurt Hutten
ad0e9c1d4d format project 2021-08-01 09:44:10 +10:00
Kurt Hutten
caf944716b Move og-image-gen out of functions folder 2021-08-01 09:42:15 +10:00
Davor Hrg
8162a182d7 Update jsCadController.ts 2021-07-31 21:03:44 +02:00
Kurt Hutten
22f3fb6e3e Remove og image types 2021-08-01 04:45:43 +10:00
Kurt Hutten
6d68b939be try moving things into dev dependencies 2021-07-31 17:26:10 +10:00
Kurt Hutten
5c673a6a01 attempt to got functions to deploy 2021-07-31 17:04:20 +10:00
Kurt Hutten
2497627c1d Merge pull request #423 from Irev-Dev/kurt/97-preview-image-rebase
preview image
2021-07-31 16:27:14 +10:00
Kurt Hutten
cbaa79b697 add netlify builder to capture social images
plus also added an invalidator that should invalidate the image each
month
2021-07-31 16:06:22 +10:00
Kurt Hutten
e4bf8f5e81 Add social preview page 2021-07-31 15:34:03 +10:00
Davor Hrg
73cbb9dbb3 stl export for jscad 2021-07-28 15:23:52 +02:00
Davor Hrg
2a3fb0fd84 fix for react-fiber not properly cleaning the scene 2021-07-28 14:27:50 +02:00
Davor Hrg
8d0f2fca51 light tweaks 2021-07-27 12:37:29 +02:00
Davor Hrg
6e88e7030d Update jsCadController.ts 2021-07-27 10:47:29 +02:00
Davor Hrg
0b769a6524 something works 2021-07-27 10:36:02 +02:00
Kurt Hutten
3b1bb23563 getting some geometry into the 3d view 2021-07-27 07:48:56 +10:00
Davor Hrg
1479832b51 stupid typo 2021-07-26 23:01:51 +02:00
Davor Hrg
438a9135e4 unhealthy on err 2021-07-26 22:57:22 +02:00
Davor Hrg
1bfba591ea initial scrappy integration 2021-07-26 22:43:51 +02:00
Kurt Hutten
de71b8f67c Demo branch adding JSCAD type, without implementing the render
Related to #411
2021-07-25 20:59:53 +10:00
Kurt Hutten
3f310a9aaf Merge pull request #421 from Irev-Dev/kurt/420
Fix subscriber name When signing up to newsletter
2021-07-25 20:18:34 +10:00
Kurt Hutten
68da3ce2c1 Fix subscriber name When signing up to newsletter
Resolves #420
2021-07-25 20:16:55 +10:00
Kurt Hutten
3e952e3b36 Update README.md 2021-07-19 05:48:43 +10:00
Kurt Hutten
3819e4313d Add aspect-ratio to docs tailwind 2021-07-18 20:16:29 +10:00
Kurt Hutten
76542a8032 Merge pull request #415 from Irev-Dev/kurt/409-update-title-and-more
Kurt/409 update title and more
2021-07-18 20:10:40 +10:00
Kurt Hutten
90e4d84865 Enforce 25 character titles on front end 2021-07-18 20:08:05 +10:00
Kurt Hutten
754436c79d Make sure old links still work in the dev-IdeWrapper
by making it case-insensitive, forcing it to lower case.
2021-07-18 19:50:39 +10:00
Kurt Hutten
b0bdb2c6f6 Fix canvas jumping on screen cap 2021-07-18 19:28:03 +10:00
Kurt Hutten
5a045fd89d solve weird issues where seperate query would fire each others oncempleted 2021-07-18 18:55:55 +10:00
Kurt Hutten
0100836e31 Fix renaming project title 2021-07-18 18:32:58 +10:00
Kurt Hutten
53eaa2e0f4 Remove console noise 2021-07-18 18:16:28 +10:00
Kurt Hutten
9e968479fc Formatting after #411 2021-07-18 08:37:48 +10:00
Kurt Hutten
23b75f56b5 Merge pull request #412 from Irev-Dev/kurt/cad-kernal-demo-411
Add demo CAD kernel Controller, and typing to suit
2021-07-18 08:26:12 +10:00
Kurt Hutten
21608b740a Add demo CAD kernel Controller, and typing to suit
We might be adding JSCAD soon and adding some guidance on the happy
path with good typing for the CadKernels is a good idea

related to #411
2021-07-18 08:24:32 +10:00
Kurt Hutten
62ec8159b8 Merge pull request #407 from Irev-Dev/kurt/400-rebase
massive refactor  toDrop cascadeStudio and add CadQuery + OpenSCAD
2021-07-17 17:52:33 +10:00
Kurt Hutten
8e558d2342 massive refactor toDrop cascadeStudio and add CadQuery + OpenSCAD
resolves #400
2021-07-17 17:47:29 +10:00
Kurt Hutten
477a557eb8 Merge pull request #406 from Irev-Dev/release
Release
2021-07-17 17:34:00 +10:00
Kurt Hutten
78a901f2f6 Merge pull request #405 from Irev-Dev/main
Release 13th July 2021
2021-07-13 05:24:13 +10:00
Kurt Hutten
6a1730bd2b Merge pull request #401 from Irev-Dev/franknoirot/blog-post
UX case studies intro post.
2021-07-13 05:15:30 +10:00
Kurt Hutten
7507b90561 Update README.md 2021-07-12 19:19:52 +10:00
Kurt Hutten
75cd72d028 Update README.md 2021-07-12 19:19:16 +10:00
Kurt Hutten
066c451f0c Update homepages 2021-07-12 19:17:23 +10:00
Frank Noirot
9ab234f6d1 Lengthened end paragraph with "stay tuned" message. 2021-07-11 17:40:23 -04:00
Kurt Hutten
92c7871659 Merge pull request #403 from Irev-Dev/main
Release 7th July 2021
2021-07-07 20:00:28 +10:00
Kurt Hutten
ff4a453ea9 Merge pull request #402 from Irev-Dev/kurt/fix-small-screens
Fix small screens
2021-07-07 19:59:16 +10:00
Kurt Hutten
6ed315a7ae Fix small screens 2021-07-07 19:54:45 +10:00
Frank Noirot
005dbdebec Wording tweak from 'we believe' to question 2021-07-06 10:19:33 -04:00
Frank Noirot
50a00e1028 rough draft of UX case studies intro post. 2021-07-06 02:14:58 -04:00
Kurt Hutten
17dfdac0c1 Merge pull request #399 from Irev-Dev/main
Release 4th July 2021
2021-07-04 20:50:36 +10:00
Kurt Hutten
e30f870e92 Merge pull request #396 from Irev-Dev/kurt/gzip-391
Gzip files before upload to s3 + high quality CadQuery downloads + TS conversions
2021-07-04 20:49:21 +10:00
Kurt Hutten
f176bbe090 Use high quality render for CadQuery download 2021-07-04 20:48:11 +10:00
Kurt Hutten
70e55a039d Implement gzip compression for cad artifacts
The stls from CadQuery and OpenSCAD are not compressed and so we're
throwing away bandwidth and taking a performance hit by not gziping.

Gzip for s3 basically needs to be gziped before upload and than have

'content-type'      :  'text/stl'
'content-encoding'  :  'gzip'
set.

https://stackoverflow.com/questions/8080824/how-to-serve-gzipped-assets-from-amazon-s3

The obvious part that needs to change is putObject in
app/api/src/docker/common/utils.js but there might be a few more
nuances.

resolves #391
2021-07-03 08:25:20 +10:00
Kurt Hutten
207eb6790a Update curated-code-cad to add live editor links for OpenSCAD & CadQuery 2021-07-02 21:07:16 +10:00
Kurt Hutten
2e3f9b9e25 Add deprecation warning for CascadeStudio
https://github.com/Irev-Dev/cadhub/discussions/261
2021-07-02 20:38:05 +10:00
Kurt Hutten
b01c08b0d2 Remove email route after all of the sillyness 2021-07-02 20:18:17 +10:00
Kurt Hutten
5def8dfd42 Merge pull request #395 from Irev-Dev/main
Emergency fix for cad base url
2021-07-01 20:59:29 +10:00
Kurt Hutten
5a4588ac7f Emergency fix for cad base url 2021-07-01 20:58:30 +10:00
Kurt Hutten
744016972e Merge pull request #394 from Irev-Dev/main
Release 1st July 2021
2021-07-01 20:30:15 +10:00
Kurt Hutten
0531e2cb54 Remove insult on home-page and add github to footer 2021-07-01 20:24:02 +10:00
Kurt Hutten
9671505a49 Merge pull request #393 from Irev-Dev/kurt/email-388
Update email code to send an email per EditUserCell
2021-07-01 20:15:36 +10:00
Kurt Hutten
084c4afdc3 Update email code to send an email per EditUserCell
using multiple recipients is break of privacy since user will be able
to see each other's emails.
2021-07-01 20:14:46 +10:00
Kurt Hutten
43376b8214 Add build path back for openscad 2021-07-01 07:08:48 +10:00
Kurt Hutten
426c3549c6 Merge pull request #392 from Irev-Dev/kurt/latest-scad-321
Upgrade openscad to 2021.01
2021-07-01 07:02:17 +10:00
Kurt Hutten
fc7c74f3e1 Upgrade openscad to 2021.01
resolves #321
2021-07-01 06:59:53 +10:00
Kurt Hutten
37dc0278fa Merge pull request #390 from Irev-Dev/main
release 29th June 2021
2021-06-29 18:57:26 +10:00
Kurt Hutten
8f8dae70c2 Deploy optimized CadQuery stls 2021-06-29 18:43:43 +10:00
Kurt Hutten
8fec470e9f Improve aws/docker docs a little. 2021-06-29 17:34:03 +10:00
Kurt Hutten
7417499d4b Formatting 2021-06-29 17:17:21 +10:00
Kurt Hutten
726945c2ab Merge pull request #389 from Irev-Dev/kurt/388-email
Add emails
2021-06-29 17:07:31 +10:00
Kurt Hutten
98d1b0643d Add admin email notifications 2021-06-29 16:21:28 +10:00
Kurt Hutten
0da15443cb Add email integration to be able to broadcast emails to all users.
Resolves #388
2021-06-29 06:37:04 +10:00
Kurt Hutten
b1dcd46a33 Merge pull request #387 from Irev-Dev/main
Release 28 June 2021
2021-06-28 20:51:55 +10:00
Kurt Hutten
7ef8d8d1ff Merge branch 'main' of github.com:Irev-Dev/cadhub 2021-06-28 20:49:30 +10:00
Kurt Hutten
f9c3f5ed25 Add blog to footer 2021-06-28 20:48:29 +10:00
Kurt Hutten
fc912279b6 Merge pull request #386 from t-paul/patch-1
Update variable name for lambda base URL.
2021-06-27 14:22:42 +10:00
Torsten Paul
c0b961ee39 Update variable name for lambda base URL. 2021-06-27 02:19:55 +02:00
Kurt Hutten
1ed1f3a2b6 Add more details to the docker readme 2021-06-27 07:38:19 +10:00
Kurt Hutten
2baf11fc11 Add material 2021-06-27 06:35:58 +10:00
Kurt Hutten
f93f5e30d8 Merge pull request #384 from Irev-Dev/kurt/383-lighting
Improve three.js scene lighting
2021-06-27 06:32:52 +10:00
Kurt Hutten
c56cf6f4ce Improve three.js scene lighting
Related to #383
2021-06-27 06:23:28 +10:00
Kurt Hutten
e95d47cfdf Update default cq stl quality 2021-06-27 05:52:37 +10:00
Kurt Hutten
bae5ba9d20 Merge pull request #382 from jmwright/main
Updated for cq-cli beta 2 of v2.2, which adds the ability to adjust STL quality
2021-06-26 20:11:06 +10:00
Jeremy Wright
f84d4cacee Updated for cq-cli beta 2 of v2.2, which adds the ability to adjust STL quality 2021-06-25 20:14:10 -04:00
Kurt Hutten
e91a264768 lint project 2021-06-26 06:14:49 +10:00
Kurt Hutten
ce210f8805 Merge pull request #380 from Irev-Dev/main
Release 25th June 2021
2021-06-25 19:48:46 +10:00
Kurt Hutten
0845575680 Merge pull request #379 from Irev-Dev/kurt/378
Fix 502 lambda cors issue. deploy new cq-cli and images tweaks
2021-06-25 19:46:39 +10:00
Kurt Hutten
29999bc2ce Allow admins to edit parts
Needed for moderation
2021-06-25 19:43:34 +10:00
Kurt Hutten
87e43ab7ce Add Cors headers to lamda timeout so they can be detected on the FE 2021-06-25 19:26:33 +10:00
Kurt Hutten
0773915fbc Update cq-cli to latest (beta) 2021-06-25 19:26:12 +10:00
Kurt Hutten
41862257ac Merge pull request #377 from jmwright/main
Added libglew 2.1 to Dockerfile install line
2021-06-25 08:43:37 +10:00
Jeremy Wright
3cebb100c7 Added libglew 2.1 to Dockerfile install line 2021-06-24 17:35:07 -04:00
Kurt Hutten
7a98afa8df Merge pull request #374 from jmwright/main
Updated Dockerfile for newer version of CadQuery and cq-cli
2021-06-25 05:43:04 +10:00
Kurt Hutten
bbab35e05b Merge pull request #376 from Irev-Dev/kurt/375-missing-images
missing images
2021-06-25 05:36:49 +10:00
Kurt Hutten
473f8d667c Fix images being deleted when they shouldn't
Resolves #375
2021-06-25 05:34:17 +10:00
Jeremy Wright
8fa779bb32 Updated Dockerfile for newer version of CadQuery and cq-cli 2021-06-24 10:09:19 -04:00
Kurt Hutten
52bf8922c4 Lint project 2021-06-24 20:36:56 +10:00
Kurt Hutten
df5fc0a100 regenerate lock file 2021-06-24 20:25:51 +10:00
Kurt Hutten
b7ee282238 Add IDE screen shot 2021-06-24 19:44:59 +10:00
Kurt Hutten
2e6afa86f6 Update landing page links 2021-06-23 19:37:23 +10:00
Kurt Hutten
69a34301e7 Add blog posts 2021-06-23 07:16:21 +10:00
Kurt Hutten
8887092606 3d-diffs 2021-06-20 16:38:35 +10:00
Kurt Hutten
0e20a06731 Merge pull request #371 from Irev-Dev/release
sync main with release after debugging email verification
2021-06-19 15:36:05 +10:00
Kurt Hutten
ec6919d499 import graphql for reasons 2021-06-19 11:23:42 +10:00
Kurt Hutten
3e0f17ee3c Upgrade redwood 34.1 2021-06-19 11:09:13 +10:00
Kurt Hutten
13f769af5e Add api logs back in 2021-06-19 10:30:12 +10:00
Kurt Hutten
35a1d84951 Explicitly add graphql-tag as api dependency 2021-06-19 08:51:42 +10:00
Kurt Hutten
13b5d9c108 Remove all logs from api 2021-06-19 07:54:21 +10:00
Kurt Hutten
7f588ebedf Try removing logs 2021-06-19 05:56:34 +10:00
Kurt Hutten
142bf2c890 Start adding ide colors, includes adding custom theme to openscad
Resolves #365
2021-06-16 21:49:24 +10:00
Kurt Hutten
51589f6c0a Merge pull request #362 from Irev-Dev/kurt/update-ide-panel-toolbar-360
IDE redesign, initial implementation
2021-06-15 18:05:02 +10:00
Kurt Hutten
155923b2e7 Add reset view to view dropdown in EditorMenu
Related to #360
2021-06-14 09:21:47 +10:00
Kurt Hutten
b65c4530b2 Tweak panel toolbars
See this thread:
https://discord.com/channels/775984095250612234/775984095250612240/853751444703543326

Related to #360
2021-06-14 08:49:52 +10:00
Kurt Hutten
d3cb93b218 Add docs for how to use external links
related to #360
2021-06-14 06:29:26 +10:00
Kurt Hutten
5083d8e7f8 Add external resource flow
related to #360
2021-06-13 17:08:37 +10:00
Kurt Hutten
da81942adc Run linting
related to #360
2021-06-13 06:33:03 +10:00
Kurt Hutten
d781c94027 Convert to UseIdeContext
related to #360
2021-06-13 06:23:36 +10:00
Kurt Hutten
c142860433 Move encoded script logic together 2021-06-13 06:17:01 +10:00
Kurt Hutten
e591eb8ff8 Rename IdeToolbarNew to IdeWrapper
related to #360
2021-06-12 20:57:04 +10:00
Kurt Hutten
f032dd1e90 Clean up IdeToolbarNew 2021-06-12 20:39:33 +10:00
Kurt Hutten
e9583166f4 Move context provide up one layer to the DevIdePage
Related to #360
2021-06-12 20:35:13 +10:00
Kurt Hutten
cd94f11374 Disable buttons that can't be used yet
Also make construction message hidable.
related to #360
2021-06-12 20:10:30 +10:00
Kurt Hutten
6b4ff7aa61 Make new favicon into a homepage link
related to #360
2021-06-12 19:24:42 +10:00
Kurt Hutten
3c18a24cb6 Add proper menu
related to #360
2021-06-12 19:20:24 +10:00
Kurt Hutten
6ad731d158 finish ide lay out
added side bar and editor toolbar
related to #360
2021-06-11 19:47:25 +10:00
Kurt Hutten
81f7659bbc Convert Svg component to typescript
related to #360
2021-06-11 19:31:46 +10:00
Kurt Hutten
ab084e0d5a Add rough IdeHeader
relates to #360
2021-06-09 06:43:12 +10:00
Kurt Hutten
02460544bf Add new viewer and console toolbar (handle + settings)
Related to #360
2021-06-08 19:27:56 +10:00
Kurt Hutten
927ba29e04 Merge pull request #359 from Irev-Dev/kurt/typo
Fix typo
2021-06-08 06:05:13 +10:00
Kurt Hutten
be2eb8215a Fix typo 2021-06-08 06:04:05 +10:00
Kurt Hutten
11660a852f Merge pull request #358 from Irev-Dev/kurt/polyround-line-highlighting
Add light highlighting to round anything docs
2021-06-07 20:38:23 +10:00
Kurt Hutten
ec68b8827c Add light highlighting to round anything docs 2021-06-07 20:37:38 +10:00
Kurt Hutten
d93326f672 Merge pull request #357 from Irev-Dev/kurt/update-docs-version-356
Update docusaurus to 2.0.0-beta.0
2021-06-07 20:23:48 +10:00
Kurt Hutten
6fcbf58af5 Update docusaurus to 2.0.0-beta.0 2021-06-07 20:22:29 +10:00
Kurt Hutten
8f60a29fc7 Merge pull request #354 from Irev-Dev/kurt/round-anything-docs-351
Add openscad review as blog past
2021-06-07 20:07:23 +10:00
Kurt Hutten
eeb8071fec Add openscad review as blog past 2021-06-07 20:06:08 +10:00
Kurt Hutten
9e4527245d Merge pull request #352 from Irev-Dev/kurt/round-anything-docs-351
Initial migration of round-anything docs into CadHub
2021-06-07 17:36:13 +10:00
Kurt Hutten
18f78a463d Initial migration of round-anything docs into CadHub
Resolves 351.
2021-06-07 17:32:27 +10:00
Kurt Hutten
15732a9ed7 Merge pull request #337 from Irev-Dev/kurt/docs-update-336
Update read me to reflect up and coming OpenSCAD and CadQuery integrations
2021-06-06 20:06:07 +10:00
Kurt Hutten
8c8bd22664 Fix sentry org and project details
Still not sure though
Related to #343
2021-06-06 06:56:22 +10:00
Kurt Hutten
e67eba61aa Put netlify plugin info in the right toml file
related to #343
2021-06-06 06:36:43 +10:00
Kurt Hutten
7cf4f801f4 Merge pull request #347 from Irev-Dev/kurt/toaster-346
Replace redwood flash with toaster
2021-06-06 06:14:21 +10:00
Kurt Hutten
a0b588000a Replace redwood flash with toaster
https://community.redwoodjs.com/t/redwood-flash-is-being-replaced-with-react-hot-toast-how-to-update-your-project-v0-27-0/1921
Resolves #326.
2021-06-06 06:12:26 +10:00
Kurt Hutten
7caf857c98 Merge pull request #344 from Irev-Dev/kurt/sentry-343
Add initial sentry setup
2021-06-05 20:35:17 +10:00
Kurt Hutten
2e91c74baf Add initial sentry setup
Related to #343 but will probably need a few more changes
2021-06-05 20:32:56 +10:00
Kurt Hutten
c38f94558a Disable esbuild
Related to #341
2021-06-05 13:06:55 +10:00
Kurt Hutten
c86a9d39f2 Merge pull request #342 from Irev-Dev/kurt/341-upgade-redwood
Upgrade redwood
2021-06-05 12:53:58 +10:00
Kurt Hutten
563700d995 Upgrade redwood to 0.33
Resolves #341.
2021-06-05 12:52:39 +10:00
Kurt Hutten
e433648fe6 Upgrade redwood to 0.32.2 2021-06-05 12:32:13 +10:00
Kurt Hutten
4a351385e1 Merge pull request #339 from Irev-Dev/kurt/338-Round-anything-library
Add experimental support for OpenSCAD libraries
2021-06-05 12:22:11 +10:00
Kurt Hutten
b868ab292b Add experimental support for OpenSCAD libraries
I've been able to get a proof of concept of downloading a openscad
library when the docker image builds
https://twitter.com/IrevDev/status/1400785325509660678
Since its experimental atm I'll leave it with just the one for now.

I've also got a local dev working again for the cad lambdas.

Resolves #338
2021-06-05 12:20:53 +10:00
Kurt Hutten
77d0fcb336 Update read me to reflect openscad and cadquery changes
Resolves #336.
2021-06-04 18:24:14 +10:00
Kurt Hutten
a2327b6169 Merge pull request #335 from Irev-Dev/kurt/improve-caching-334
Improve browser caching with cache control header
2021-05-31 17:00:28 +10:00
Kurt Hutten
913045fe12 Improve browser caching with cache control header
Not only does the header need to be added, but the signed URL needs to
have it's expiry rounded so that the return url is the same for a given
window, say 10minutes

I followed this https://advancedweb.hu/cacheable-s3-signed-urls/
basically what this means is that because we're caching the assets
themselves, if as user asks for a part that already exists we'll return
a url for the existing part instead of regenerating it, however if it
was them that generated the part less than 10 minutes ago, they'll still
have to download the asset again. This way it will save us costs and
will be quicker for them.

Resolves #334
2021-05-31 16:56:21 +10:00
Kurt Hutten
c049a1d3db Merge pull request #333 from Irev-Dev/kurt/fix-init-code-bug
Make sure the code-init is robust when local storage is empty
2021-05-30 14:01:52 +10:00
Kurt Hutten
bd7aa4cc4e Make sure the code-init is robust when local storage is empty
Plus fix local storage for old code
2021-05-30 13:59:16 +10:00
Kurt Hutten
d7aaeda187 Merge pull request #331 from Irev-Dev/kurt/330-stl-download
Add stl download for OpenSCAD and CadQuery IDEs
2021-05-30 08:47:31 +10:00
Kurt Hutten
bd58e6c7cb Add stl download for OpenSCAD and CadQuery IDEs
Resolves #330.
2021-05-30 08:11:46 +10:00
Kurt Hutten
32fa22efcd Merge pull request #328 from Irev-Dev/kurt/327
Add the ability to link to text resource for IDE
2021-05-28 06:59:52 +10:00
Kurt Hutten
f629833229 Add the ability to link to text resource for IDE
Very basic feature, puts the url of the resource in the url for the ide
to fetch when it loads. I haven't added the ability to create these
links as it probably needs some consideration with how it fits into the
IDE.

Should work with any url that returns text but obviously has to CORS
enable since it's in the browser, of which gitlab raw content is not
which is kinda bizzare.

works with github raw content like so:
`#fetch_text_v1=https://raw.githubusercontent.com/aaevan/openscad_objects/main/fire_tablet_bottom_corner.scad`
however I would recommend url encoding it with `encodeURIComponent` in
case there are special characters in the path
`#fetch_text_v1=https%3A%2F%2Fraw.githubusercontent.com%2Faaevan%2Fopenscad_objects%2Fmain%2Ffire_tablet_bottom_corner.scad`

In the case of github, linking to raw is the safest, however it will try
and get to the raw content from the web-app url i.e. the following still
works:
`#fetch_text_v1=https://github.com/aaevan/openscad_objects/blob/main/fire_tablet_bottom_corner.scad`

Resolves #327
2021-05-28 06:45:39 +10:00
Kurt Hutten
d3f7b40a9b Merge pull request #325 from Irev-Dev/kurt/blog-why-i-started-cadhub
Add "Why I started cadhub" blog post
2021-05-22 06:45:00 +10:00
Kurt Hutten
8ed814ac9f Add "Why I started cadhub" blog post 2021-05-22 06:41:55 +10:00
Kurt Hutten
90f6808663 Merge pull request #322 from Irev-Dev/kurt/link-to-docs
Link to docs
2021-05-18 21:38:50 +10:00
Kurt Hutten
644e431eba Link to docs
Not much connection between the main website and the docs website atm
2021-05-18 21:36:33 +10:00
Kurt Hutten
31b1eb594e Merge pull request #317 from Irev-Dev/kurt/s3-integration-316-rebase
Add s3 integration
2021-05-18 07:19:48 +10:00
Kurt Hutten
315492a08a Add s3 integration
Doing so has a number of benefits
- Overcome the 10Mb limit of the API gateway the lambdas have to go
through
- By storing the key as the hash of the code we can return previous
generated assets, i.e. caching
- cost, transfering assets into the bucket within the AWS ecosystem
is faster than return, and there fore the lambdas execute for less time
- Sets us up for the future as when generating artifacts for repos when
there is a change to master etc we want to store these assets somewhere
and s3 is an obvious choice
- Solved a weird CORS issue where I couldn't get CORS working with
binaryMediaTypes enabled, don't need binary types when dumping in s3

Resolves #316
2021-05-18 07:13:08 +10:00
Kurt Hutten
027b45e6c3 Merge pull request #314 from Irev-Dev/kurt/add-live-demo-links-313
Add live demo links to OpenSCAD tutorial
2021-05-08 22:32:55 +10:00
Kurt Hutten
8a5728508c Add live demo links to OpenSCAD tutorial
Resolves #313
2021-05-08 22:31:43 +10:00
Kurt Hutten
a08426e078 Merge pull request #312 from Irev-Dev/kurt/improve-script-url-encoding-311
Improve script url encoding
2021-05-08 09:15:49 +10:00
Kurt Hutten
7749a14da3 All apostrophes in jsx 2021-05-08 09:09:07 +10:00
Kurt Hutten
053b1d642c Improve script URL ecoding
added some magic to get scripts to efficiently encoded into the URL.
We're using pako to compress the script, but this outputs to a 8bit
array. Stringifying this array adds a lot of overhead, because "125"
has three characters in it.
Instead we're using the character codes to turn these a bit numbers
into single characters base64 is used as well because not all of the
characters are allowed in a url (and b64 is better than
encodeURIComponent).
2021-05-08 09:07:54 +10:00
Kurt Hutten
53985dd250 Merge pull request #307 from Tobbe/patch-1
the-basics.mdx: Make text match example code
2021-05-08 05:20:33 +10:00
Kurt Hutten
cc8166183e Merge branch 'main' into patch-1 2021-05-08 05:20:00 +10:00
Kurt Hutten
9bca10b380 Merge pull request #308 from Tobbe/patch-2
the-basics.mdx: Fix punctuation formatting
2021-05-08 05:17:18 +10:00
Kurt Hutten
c50ffc5c8b Merge pull request #309 from Tobbe/tobbe-basics-translate
the-basics.mdx: Small updates to the Translate section
2021-05-08 05:16:44 +10:00
Tobbe Lundberg
277dc6c008 the-basics.mdx: Small updates to the Translate section 2021-05-07 15:08:59 +02:00
Tobbe Lundberg
81ee147f51 the-basics.mdx: Fix punctuation formatting 2021-05-07 15:03:03 +02:00
Tobbe Lundberg
f0d32d7754 the-basics.mdx: Fix punctuation formatting 2021-05-07 15:00:37 +02:00
Kurt Hutten
aba9b2e19e Merge pull request #305 from Irev-Dev/kurt/docs-polish-304
docs polish
2021-05-07 20:45:00 +10:00
Kurt Hutten
67324f0d0c Remove reference to primer 2021-05-07 20:38:26 +10:00
Kurt Hutten
7070ac21d4 Proof read docs 2021-05-07 20:38:26 +10:00
Kurt Hutten
2d828d7739 Add rounding and spacing at the bottom of images 2021-05-07 20:38:04 +10:00
320 changed files with 19194 additions and 9610 deletions

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "web/src/cascade"]
path = app/web/src/cascade
url = https://github.com/Irev-Dev/CascadeStudio.git

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

@@ -0,0 +1,11 @@
{
"cSpell.words": [
"Cadhub",
"Customizer",
"Hutten",
"cadquery",
"jscad",
"openscad",
"sendmail"
]
}

View File

@@ -2,24 +2,27 @@
# [C a d H u b](https://cadhub.xyz) # [C a d H u b](https://cadhub.xyz)
[![Netlify Status](https://api.netlify.com/api/v1/badges/77f37543-e54a-4723-8136-157c0221ec27/deploy-status)](https://app.netlify.com/sites/cadhubxyz/deploys) <!-- [![Netlify Status](https://api.netlify.com/api/v1/badges/77f37543-e54a-4723-8136-157c0221ec27/deploy-status)](https://app.netlify.com/sites/cadhubxyz/deploys) -->
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have an integration with the excellent [cascadeStudio](https://zalo.github.io/CascadeStudio/) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad). Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have integrations in progress for [OpenSCAD](https://cadhub.xyz/dev-ide/openscad) and [CadQuery](https://cadhub.xyz/dev-ide/cadquery) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
If you want to be involved in anyway, checkout the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and get in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions). If you want to be involved in anyway, checkout the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and get in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/fullcadhubshot.jpg"> <img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/fullcadhubshot.jpg">
<img src="https://raw.githubusercontent.com/Irev-Dev/cadhub/main/docs/static/img/blog/curated-code-cad/CadHubSS.jpg">
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/Part%20IDE%20-%20export%20expand%20state.jpg"> ## Getting your dev environment setup
## Getting Started
Because we're integrating cascadeStudio, this is done some what crudely for the time being, so you'll need to clone the repo with submodules.
```terminal ```terminal
git clone --recurse-submodules -j8 git@github.com:Irev-Dev/cadhub.git git clone git@github.com:Irev-Dev/cadhub.git
# or # or
git clone --recurse-submodules -j8 https://github.com/Irev-Dev/cadhub.git git clone https://github.com/Irev-Dev/cadhub.git
```
cd in the app directory
```
cd app
``` ```
Install dependencies Install dependencies
@@ -27,10 +30,7 @@ Install dependencies
yarn install yarn install
``` ```
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup) with a couple of exceptions: Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup).
- Run `yarn rw prisma migrate dev` instead of `yarn rw db up` in the guide.
- Don't worry about changing the `schema.prisma` file.
- You will need to add a `DATABASE_URL` and test url to you `.env` file at the root of the project.
Run the following Run the following
``` terminal ``` terminal
@@ -38,6 +38,8 @@ yarn rw prisma migrate dev
yarn rw prisma db seed yarn rw prisma db seed
``` ```
p.s. `yarn rw prisma studio` spins up an app to inspect the db
### Fire up dev ### Fire up dev
```terminal ```terminal
yarn rw dev yarn rw dev
@@ -45,6 +47,8 @@ yarn rw dev
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`. Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5">"`
you can sign in to the following accounts locally you can sign in to the following accounts locally
localUser1@kurthutten.com: `abc123` localUser1@kurthutten.com: `abc123`
@@ -73,6 +77,13 @@ We're using tailwind utility classes so please try and use them as much as possi
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1) In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/Part%20Page(1).jpg"> ## Integrations
The OpenSCAD and CadQuery integrations work by leveraging each of their cli tools in a docker image. It's currently deployed to AWS and can be found [here](https://github.com/Irev-Dev/cadhub/tree/main/app/api/src/docker).
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/User%20Page%20Edit.jpg"> ## Docs
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)
## Who is CadHub
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright) is a major contributor. Plus a number smaller contributors.

View File

@@ -18,8 +18,13 @@ CLOUDINARY_API_KEY=476712943135152
# trace | info | debug | warn | error | silent # trace | info | debug | warn | error | silent
# LOG_LEVEL=debug # LOG_LEVEL=debug
CAD_LAMBDA_BASE_URL="https://wzab9s632b.execute-api.us-east-1.amazonaws.com/prod"
# Github assist app # EMAIL_PASSWORD=abc123
# GITHUB_ASSIST_APP_ID=""
# GITHUB_ASSIST_SECRET=""
# CAD_LAMBDA_BASE_URL="http://localhost:8080"
# sentry
GITHUB_ASSIST_APP_ID=23342
GITHUB_ASSIST_SECRET=abc
GITHUB_ASSIST_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nabcdefg\n-----END RSA PRIVATE KEY-----"

View File

@@ -1 +1 @@
/web/src/cascade/*

3
app/.gitignore vendored
View File

@@ -1,2 +1,3 @@
dist dist
*.pem web/types/graphql.d.ts
api/types/graphql.d.ts

View File

@@ -22,9 +22,7 @@
"Uploader", "Uploader",
"describedby", "describedby",
"initialise", "initialise",
"octokit",
"redwoodjs", "redwoodjs",
"repos",
"resizer", "resizer",
"roboto", "roboto",
"ropa" "ropa"

View File

@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "RW_DataMigration" (
"version" TEXT NOT NULL,
"name" TEXT NOT NULL,
"startedAt" TIMESTAMP(3) NOT NULL,
"finishedAt" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("version")
);

View File

@@ -0,0 +1,31 @@
/*
Warnings:
- You are about to drop the `Comment` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Part` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `PartReaction` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_partId_fkey";
-- DropForeignKey
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
-- DropForeignKey
ALTER TABLE "Part" DROP CONSTRAINT "Part_userId_fkey";
-- DropForeignKey
ALTER TABLE "PartReaction" DROP CONSTRAINT "PartReaction_partId_fkey";
-- DropForeignKey
ALTER TABLE "PartReaction" DROP CONSTRAINT "PartReaction_userId_fkey";
-- DropTable
DROP TABLE "Comment";
-- DropTable
DROP TABLE "Part";
-- DropTable
DROP TABLE "PartReaction";

View File

@@ -0,0 +1,59 @@
-- CreateTable
CREATE TABLE "Project" (
"id" TEXT NOT NULL,
"title" VARCHAR(25) NOT NULL,
"description" TEXT,
"code" TEXT,
"mainImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"deleted" BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProjectReaction" (
"id" TEXT NOT NULL,
"emote" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Comment" (
"id" TEXT NOT NULL,
"text" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Project.title_userId_unique" ON "Project"("title", "userId");
-- CreateIndex
CREATE UNIQUE INDEX "ProjectReaction.emote_userId_projectId_unique" ON "ProjectReaction"("emote", "userId", "projectId");
-- AddForeignKey
ALTER TABLE "Project" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectReaction" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectReaction" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "CadPackage" AS ENUM ('openscad', 'cadquery');
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "cadPackage" "CadPackage" NOT NULL DEFAULT E'openscad';

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "SocialCard" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"url" TEXT,
"outOfDate" BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "SocialCard_projectId_unique" ON "SocialCard"("projectId");
-- AddForeignKey
ALTER TABLE "SocialCard" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterIndex
ALTER INDEX "SocialCard_projectId_unique" RENAME TO "SocialCard.projectId_unique";

View File

@@ -1,2 +1,3 @@
# Please do not edit this file manually # Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql" provider = "postgresql"

View File

@@ -1,4 +1,4 @@
datasource DS { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
@@ -14,8 +14,7 @@ generator client {
// ADMIN // ADMIN
// } // }
// enum PartType { // enum ProjectType {
// CASCADESTUDIO
// JSCAD // JSCAD
// } // }
@@ -33,15 +32,21 @@ model User {
image String? // url maybe id or file storage service? cloudinary? image String? // url maybe id or file storage service? cloudinary?
bio String? //mark down bio String? //mark down
Part Part[] Project Project[]
Reaction PartReaction[] Reaction ProjectReaction[]
Comment Comment[] Comment Comment[]
SubjectAccessRequest SubjectAccessRequest[] SubjectAccessRequest SubjectAccessRequest[]
} }
model Part { enum CadPackage {
openscad
cadquery
// jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
}
model Project {
id String @id @default(uuid()) id String @id @default(uuid())
title String title String @db.VarChar(25)
description String? // markdown string description String? // markdown string
code String? code String?
mainImage String? // link to cloudinary mainImage String? // link to cloudinary
@@ -50,23 +55,36 @@ model Part {
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String userId String
deleted Boolean @default(false) deleted Boolean @default(false)
cadPackage CadPackage @default(openscad)
socialCard SocialCard?
Comment Comment[] Comment Comment[]
Reaction PartReaction[] Reaction ProjectReaction[]
@@unique([title, userId]) @@unique([title, userId])
} }
model PartReaction { model SocialCard {
id String @id @default(uuid())
projectId String @unique
project Project @relation(fields: [projectId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
url String? // link to cloudinary
outOfDate Boolean @default(true)
}
model ProjectReaction {
id String @id @default(uuid()) id String @id @default(uuid())
emote String // an emoji emote String // an emoji
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String userId String
part Part @relation(fields: [partId], references: [id]) project Project @relation(fields: [projectId], references: [id])
partId String projectId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@unique([emote, userId, partId]) @@unique([emote, userId, projectId])
} }
model Comment { model Comment {
@@ -74,8 +92,8 @@ model Comment {
text String // the comment, should I allow mark down? text String // the comment, should I allow mark down?
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId String userId String
part Part @relation(fields: [partId], references: [id]) project Project @relation(fields: [projectId], references: [id])
partId String projectId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -91,3 +109,10 @@ model SubjectAccessRequest {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model RW_DataMigration {
version String @id
name String
startedAt DateTime
finishedAt DateTime
}

View File

@@ -50,9 +50,9 @@ async function main() {
}) })
} }
const parts = [ const projects = [
{ {
title: 'demo-part1', title: 'demo-project1',
description: '# can be markdown', description: '# can be markdown',
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox', mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
user: { user: {
@@ -62,7 +62,7 @@ async function main() {
}, },
}, },
{ {
title: 'demo-part2', title: 'demo-project2',
description: '## [hey](www.google.com)', description: '## [hey](www.google.com)',
user: { user: {
connect: { connect: {
@@ -72,39 +72,43 @@ async function main() {
}, },
] ]
existing = await db.part.findMany({where: { title: parts[0].title}}) existing = await db.project.findMany({where: { title: projects[0].title}})
if(!existing.length) { if(!existing.length) {
await db.part.create({ await db.project.create({
data: parts[0], data: projects[0],
}) })
} }
existing = await db.part.findMany({where: { title: parts[1].title}}) existing = await db.project.findMany({where: { title: projects[1].title}})
if(!existing.length) { if(!existing.length) {
await db.part.create({ await db.project.create({
data: parts[1], data: projects[1],
}) })
} }
const aPart = await db.part.findUnique({where: { const aProject = await db.project.findUnique({where: {
title_userId: { title_userId: {
title: parts[0].title, title: projects[0].title,
userId: users[0].id, userId: users[0].id,
} }
}}) }})
await db.comment.create({ await db.comment.create({
data: { data: {
text: "nice part, I like it", text: "nice project, I like it",
user: {connect: { id: users[0].id}}, userId: users[0].id,
part: {connect: { id: aPart.id}}, projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
} }
}) })
await db.partReaction.create({ await db.projectReaction.create({
data: { data: {
emote: "❤️", emote: "❤️",
user: {connect: { id: users[0].id}}, userId: users[0].id,
part: {connect: { id: aPart.id}}, projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
} }
}) })

View File

@@ -1,6 +1 @@
const { getConfig } = require('@redwoodjs/core') module.exports = require('@redwoodjs/testing/config/jest/api')
const config = getConfig({ type: 'jest', target: 'node' })
config.displayName.name = 'api'
module.exports = config

View File

@@ -3,10 +3,13 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@octokit/app": "^12.0.2", "@redwoodjs/api": "^0.34.1",
"@octokit/webhooks-types": "^3.71.1", "@sentry/node": "^6.5.1",
"@redwoodjs/api": "^0.31.0", "cloudinary": "^1.23.0",
"@redwoodjs/api-server": "^0.31.0", "human-id": "^2.0.1",
"cloudinary": "^1.23.0" "nodemailer": "^6.6.2"
},
"devDependencies": {
"@types/nodemailer": "^6.4.2"
} }
} }

View File

@@ -0,0 +1,5 @@
# The following are the env vars you need run the cad lamdas locally
# The still connect to s3 so some secrets are needed, ask Kurt and he'll set things up for you
DEV_AWS_SECRET_ACCESS_KEY=""
DEV_AWS_ACCESS_KEY_ID=""
DEV_BUCKET="cad-preview-bucket-dev-001"

View File

@@ -3,7 +3,7 @@
We're using the serverless from work for deployment We're using the serverless from work for deployment
``` ```
sls deploy sls deploy --stage stagename
``` ```
But [Kurt Hutten](https://github.com/Irev-Dev) is the only one with credentials for deployment atm, though if you wanted to set your own account you could deploy to that if you wanted to test. But [Kurt Hutten](https://github.com/Irev-Dev) is the only one with credentials for deployment atm, though if you wanted to set your own account you could deploy to that if you wanted to test.
@@ -14,6 +14,10 @@ You'll need to have Docker installed
Because of the way the docker containers to be deployed as lambdas on aws are somewhat specialised for the purpose we're using `docker-compose` to spin one up for each function/endpoint. So we've added a aws-emulation layer Because of the way the docker containers to be deployed as lambdas on aws are somewhat specialised for the purpose we're using `docker-compose` to spin one up for each function/endpoint. So we've added a aws-emulation layer
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
you will also need to create a .env in `app/api/src/docker/.env` for the following env-vars `DEV_AWS_SECRET_ACCESS_KEY, DEV_AWS_ACCESS_KEY_ID and DEV_BUCKET`. Ask @irev-dev for credentials and he can sort you out.
Then cd into this folder `cd api/src/docker` and: Then cd into this folder `cd api/src/docker` and:
```bash ```bash
@@ -26,8 +30,7 @@ After which we'll also spin up a light express server to act as an emulator to t
yarn install yarn install
yarn emulate yarn emulate
``` ```
You can now add OPENSCAD_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`) You can now add CAD_LAMBDA_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`) comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
If you change anything in the `api/src/docker/openscad` directory, you will need to stop the docker process and restart it (will be fairly quick if you're only changing the js) If you change anything in the `api/src/docker/openscad` directory, you will need to stop the docker process and restart it (will be fairly quick if you're only changing the js)

View File

@@ -10,31 +10,26 @@ app.use(cors())
const invocationURL = (port) => const invocationURL = (port) =>
`http://localhost:${port}/2015-03-31/functions/function/invocations` `http://localhost:${port}/2015-03-31/functions/function/invocations`
app.post('/openscad/preview', async (req, res) => { const makeRequest = (route, port) => [
try { route,
const { data } = await axios.post(invocationURL(5052), { async (req, res) => {
body: Buffer.from(JSON.stringify(req.body)).toString('base64'), console.log(`making post request to ${port}, ${route}`)
}) try {
res.status(data.statusCode) const { data } = await axios.post(invocationURL(port), {
res.send(data.body) body: JSON.stringify(req.body),
} catch (e) { })
res.status(500) res.status(data.statusCode)
res.send() res.send(data.body)
} } catch (e) {
}) res.status(500)
app.post('/cadquery/stl', async (req, res) => { res.send()
console.log('making post request to 5060') }
try { },
const { data } = await axios.post(invocationURL(5060), { ]
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
}) app.post(...makeRequest('/openscad/preview', 5052))
res.status(data.statusCode) app.post(...makeRequest('/openscad/stl', 5053))
res.send(data.body) app.post(...makeRequest('/cadquery/stl', 5060))
} catch (e) {
res.status(500)
res.send()
}
})
app.listen(port, () => { app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`) console.log(`Example app listening at http://localhost:${port}`)

View File

@@ -36,7 +36,10 @@ RUN npm install
# Get the distribution copy of cq-cli # Get the distribution copy of cq-cli
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip RUN apt-get install -y libglew2.1
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.2-beta.2/cq-cli-Linux-x86_64.zip
# Comment the entry above out and uncomment the one below to revert to the stable release
# RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip
RUN unzip cq-cli-Linux-x86_64.zip RUN unzip cq-cli-Linux-x86_64.zip
RUN chmod +x cq-cli/cq-cli RUN chmod +x cq-cli/cq-cli

View File

@@ -1,53 +1,57 @@
const { runCQ } = require('./runCQ') const { runCQ } = require('./runCQ')
const middy = require('middy') const middy = require('middy')
const { cors } = require('middy/middlewares') const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk')
const tk = require('timekeeper')
const {
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} = require('../common/utils')
// cors true does not seem to work in serverless.yml, perhaps docker lambdas aren't covered by that config const s3 = new AWS.S3()
// special lambda just for responding to options requests
const preflightOptions = (req, _context, callback) => {
const response = {
statusCode: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': '*',
},
}
callback(null, response)
}
const stl = async (req, _context, callback) => { const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false _context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii') const eventBody = req.body
console.log(eventBody, 'eventBody') console.log('eventBody', eventBody)
const { file, settings } = JSON.parse(eventBody) const key = `${makeHash(eventBody)}.stl`
const { error, result, tempFile } = await runCQ({ file, settings }) console.log('key', key)
if (error) {
const response = { const params = {
statusCode: 400, Bucket: process.env.BUCKET,
body: JSON.stringify({ error, tempFile }), Key: key,
} }
callback(null, response) const previousAsset = await checkIfAlreadyExists(params, s3)
} else { if (previousAsset.isAlreadyInBucket) {
console.log(`got result in route: ${result}, file is: ${tempFile}`) console.log('already in bucket')
const fs = require('fs')
const image = fs.readFileSync(`/tmp/${tempFile}/output.stl`, {
encoding: 'base64',
})
console.log(image, 'encoded image')
const response = { const response = {
statusCode: 200, statusCode: 200,
body: JSON.stringify({ body: JSON.stringify({
imageBase64: image, url: getObjectUrl(params, s3, tk),
result, consoleMessage: previousAsset.consoleMessage,
tempFile,
}), }),
} }
callback(null, response) callback(null, response)
return
} }
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await runCQ({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
})
} }
module.exports = { module.exports = {
stl: middy(stl).use(cors()), stl: middy(loggerWrap(stl)).use(cors()),
preflightOptions,
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,11 @@
"author": "Kurt Hutten <kurt@kurthutten.com>", "author": "Kurt Hutten <kurt@kurthutten.com>",
"license": "", "license": "",
"dependencies": { "dependencies": {
"aws-sdk": "^2.907.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"middy": "^0.36.0", "middy": "^0.36.0",
"nanoid": "^3.1.20" "nanoid": "^3.1.20",
"timekeeper": "2.2.0"
}, },
"devDependencies": { "devDependencies": {
"aws-lambda-ric": "^1.0.0" "aws-lambda-ric": "^1.0.0"

View File

@@ -1,15 +1,26 @@
const { makeFile, runCommand } = require('../common/utils') const { makeFile, runCommand } = require('../common/utils')
const { nanoid } = require('nanoid') const { nanoid } = require('nanoid')
module.exports.runCQ = async ({ file, settings = {} } = {}) => { module.exports.runCQ = async ({
file,
settings: { deflection = 0.3 } = {},
} = {}) => {
const tempFile = await makeFile(file, '.py', nanoid) const tempFile = await makeFile(file, '.py', nanoid)
const command = `cq-cli/cq-cli --codec stl --infile /tmp/${tempFile}/main.py --outfile /tmp/${tempFile}/output.stl` const fullPath = `/tmp/${tempFile}/output.stl`
const command = [
`cq-cli/cq-cli`,
`--codec stl`,
`--infile /tmp/${tempFile}/main.py`,
`--outfile ${fullPath}`,
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
`&& gzip ${fullPath}`,
].join(' ')
console.log('command', command) console.log('command', command)
try { try {
const result = await runCommand(command, 30000) const consoleMessage = await runCommand(command, 30000)
return { result, tempFile } return { consoleMessage, fullPath }
} catch (error) { } catch (error) {
return { error, tempFile } return { error, fullPath }
} }
} }

View File

@@ -1,386 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
mime-types@~2.1.24:
version "2.1.29"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
dependencies:
mime-db "1.46.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@@ -1,6 +1,19 @@
const { exec } = require('child_process') const { exec } = require('child_process')
const { promises } = require('fs') const { promises } = require('fs')
const { writeFile } = promises const { writeFile } = promises
const { createHash } = require('crypto')
const CONSOLE_MESSAGE_KEY = 'console-message-b64'
function putConsoleMessageInMetadata(consoleMessage) {
return {
[CONSOLE_MESSAGE_KEY]: Buffer.from(consoleMessage, 'utf-8').toString(
'base64'
),
}
}
function getConsoleMessageFromMetadata(metadata) {
return Buffer.from(metadata[CONSOLE_MESSAGE_KEY], 'base64').toString('utf-8')
}
async function makeFile(file, extension = '.scad', makeHash) { async function makeFile(file, extension = '.scad', makeHash) {
const tempFile = 'a' + makeHash() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug: const tempFile = 'a' + makeHash() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
@@ -35,7 +48,118 @@ async function runCommand(command, timeout = 5000) {
}) })
} }
function makeHash(script) {
return createHash('sha256').update(script).digest('hex')
}
async function checkIfAlreadyExists(params, s3) {
try {
const objectHead = await s3.headObject(params).promise()
const consoleMessage = getConsoleMessageFromMetadata(objectHead.Metadata)
console.log('consoleMessage', consoleMessage)
return { isAlreadyInBucket: true, consoleMessage }
} catch (e) {
console.log("couldn't find it", e)
return { isAlreadyInBucket: false }
}
}
function getObjectUrl(params, s3, tk) {
const getTruncatedTime = () => {
const currentTime = new Date()
const d = new Date(currentTime)
d.setMinutes(Math.floor(d.getMinutes() / 10) * 10)
d.setSeconds(0)
d.setMilliseconds(0)
return d
}
const HALF_HOUR = 1800
return tk.withFreeze(getTruncatedTime(), () =>
s3.getSignedUrl('getObject', {
...params,
Expires: HALF_HOUR,
})
)
}
function loggerWrap(handler) {
return (req, _context, callback) => {
try {
return handler(req, _context, callback)
} catch (e) {
console.log('error in handler', e)
}
}
}
async function storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
}) {
if (error) {
const response = {
statusCode: 400,
body: JSON.stringify({ error, fullPath }),
}
callback(null, response)
return
} else {
console.log(`got result in route: ${consoleMessage}, file is: ${fullPath}`)
const { readFile } = require('fs/promises')
let buffer
try {
buffer = await readFile(`${fullPath}.gz`)
} catch (e) {
console.log('read file error', e)
const response = {
statusCode: 400,
body: JSON.stringify({ error: consoleMessage, fullPath }),
}
callback(null, response)
return
}
const FiveDays = 432000
const storedRender = await s3
.putObject({
Bucket: process.env.BUCKET,
Key: key,
Body: buffer,
CacheControl: `max-age=${FiveDays}`, // browser caching to stop downloads of the same part
ContentType: 'text/stl',
ContentEncoding: 'gzip',
Metadata: putConsoleMessageInMetadata(consoleMessage),
})
.promise()
console.log('stored object', storedRender)
const url = getObjectUrl(params, s3, tk)
console.log('url', url)
const response = {
statusCode: 200,
body: JSON.stringify({
url,
consoleMessage,
}),
}
callback(null, response)
return
}
}
module.exports = { module.exports = {
runCommand, runCommand,
makeFile, makeFile,
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} }

View File

@@ -1,44 +1,37 @@
services: services:
# aws-emulator:
# build: .
# networks:
# - awsland
# ports:
# - "5050:8080"
openscad-health:
build:
context: ./
dockerfile: ./openscad/.
image: openscad
command: openscad.health
ports:
- "5051:8080"
openscad-preview: openscad-preview:
build:
context: ./
dockerfile: ./openscad/Dockerfile
image: openscad image: openscad
# build: ./openscad/.
command: openscad.preview command: openscad.preview
# networks:
# - awsland
ports: ports:
- "5052:8080" - "5052:8080"
environment:
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
openscad-stl: openscad-stl:
image: openscad image: openscad
# build: ./openscad/.
command: openscad.stl command: openscad.stl
ports: ports:
- "5053:8080" - "5053:8080"
environment:
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
cadquery-stl: cadquery-stl:
build: build:
context: ./ context: ./
dockerfile: ./cadquery/. dockerfile: ./cadquery/Dockerfile
command: cadquery.stl command: cadquery.stl
ports: ports:
- 5060:8080 - 5060:8080
environment:
AWS_SECRET_ACCESS_KEY: "${DEV_AWS_SECRET_ACCESS_KEY}"
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
# networks:
# awsland:
# name: awsland

View File

@@ -6,9 +6,10 @@ ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -qq RUN apt-get update -qq
# double check this below, I'm not sure we need inkscape etc # double check this below, I'm not sure we need inkscape etc
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb imagemagick unzip inkscape RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb imagemagick unzip inkscape
RUN add-apt-repository ppa:openscad/releases
RUN apt-get update -qq RUN apt-get update -qq
RUN apt-get install -y -qq openscad RUN apt-get install -y -qq openscad
RUN apt-get install -y curl RUN apt-get install -y curl wget
# install node14, see comment at the to of node14source_setup.sh # install node14, see comment at the to of node14source_setup.sh
ADD common/node14source_setup.sh /nodesource_setup.sh ADD common/node14source_setup.sh /nodesource_setup.sh
@@ -37,6 +38,14 @@ WORKDIR /var/task/
COPY openscad/package*.json /var/task/ COPY openscad/package*.json /var/task/
RUN npm install RUN npm install
# Install OpenSCAD libraries
# It's experimental, so only adding latest Round-Anything for now
RUN echo "OPENSCADPATH=/var/task/openscad" >>/etc/profile && \
wget -P /var/task/openscad/ https://github.com/Irev-Dev/Round-Anything/archive/refs/tags/1.0.4.zip && \
unzip /var/task/openscad/1.0.4
# Add our own theming (based on DeepOcean with a different "background" and "opencsg-face-back")
COPY openscad/cadhubtheme.json /usr/share/openscad/color-schemes/render/
COPY openscad/*.js /var/task/ COPY openscad/*.js /var/task/
COPY common/*.js /var/common/ COPY common/*.js /var/common/
COPY common/entrypoint.sh /entrypoint.sh COPY common/entrypoint.sh /entrypoint.sh

View File

@@ -0,0 +1,19 @@
{
"name" : "CadHub",
"index" : 1600,
"show-in-gui" : true,
"colors" : {
"background" : "#1A1A1D",
"axes-color" : "#c1c1c1",
"opencsg-face-front" : "#eeeeee",
"opencsg-face-back" : "#8732F2",
"cgal-face-front" : "#eeeeee",
"cgal-face-back" : "#0babc8",
"cgal-face-2d" : "#9370db",
"cgal-edge-front" : "#0000ff",
"cgal-edge-back" : "#0000ff",
"cgal-edge-2d" : "#ff00ff",
"crosshair" : "#f0f0f0"
}
}

View File

@@ -1,95 +1,126 @@
const { runScad, stlExport } = require('./runScad') const { runScad, stlExport } = require('./runScad')
const middy = require('middy') const middy = require('middy')
const { cors } = require('middy/middlewares') const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk')
const tk = require('timekeeper')
const {
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} = require('../common/utils')
const health = async () => { const s3 = new AWS.S3()
console.log('Health endpoint')
return {
statusCode: 200,
body: 'ok',
}
}
// cors true does not seem to work in serverless.yml, perhaps docker lambdas aren't covered by that config const openScadStlKey = (eventBody) => {
// special lambda just for responding to options requests const { file } = JSON.parse(eventBody)
const preflightOptions = (req, _context, callback) => { return `${makeHash(JSON.stringify(file))}.stl`
const response = {
statusCode: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': '*',
},
}
callback(null, response)
} }
const preview = async (req, _context, callback) => { const preview = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false _context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii') const eventBody = req.body
console.log(eventBody, 'eventBody') console.log('eventBody', eventBody)
const { file, settings } = JSON.parse(eventBody) const key = `${makeHash(eventBody)}.png`
const { error, result, tempFile } = await runScad({ file, settings }) const stlKey = openScadStlKey(eventBody)
if (error) {
const response = { console.log('key', key)
statusCode: 400,
body: JSON.stringify({ error, tempFile }), const stlParams = {
} Bucket: process.env.BUCKET,
callback(null, response) Key: stlKey,
} else { }
console.log(`got result in route: ${result}, file is: ${tempFile}`)
const fs = require('fs') const params = {
const image = fs.readFileSync(`/tmp/${tempFile}/output.png`, { Bucket: process.env.BUCKET,
encoding: 'base64', Key: key,
}) }
console.log(image, 'encoded image') const [previousAssetStl, previousAssetPng] = await Promise.all([
checkIfAlreadyExists(stlParams, s3),
checkIfAlreadyExists(params, s3),
])
const type = previousAssetStl.isAlreadyInBucket ? 'stl' : 'png'
const previousAsset = previousAssetStl.isAlreadyInBucket
? previousAssetStl
: previousAssetPng
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const response = { const response = {
statusCode: 200, statusCode: 200,
body: JSON.stringify({ body: JSON.stringify({
imageBase64: image, url: getObjectUrl(
result, {
tempFile, Bucket: process.env.BUCKET,
Key: previousAssetStl.isAlreadyInBucket ? stlKey : key,
},
s3,
tk
),
consoleMessage:
previousAsset.consoleMessage || previousAssetPng.consoleMessage,
type,
}), }),
} }
callback(null, response) callback(null, response)
return
} }
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await runScad({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
})
} }
const stl = async (req, _context, callback) => { const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false _context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii') const eventBody = req.body
console.log(eventBody, 'eventBody') console.log(eventBody, 'eventBody')
const { file } = JSON.parse(eventBody) const stlKey = openScadStlKey(eventBody)
const { error, result, tempFile } = await stlExport({ file })
if (error) { console.log('key', stlKey)
const response = {
statusCode: 400, const params = {
body: { error, tempFile }, Bucket: process.env.BUCKET,
} Key: stlKey,
callback(null, response) }
} else { console.log('original params', params)
console.log(`got result in route: ${result}, file is: ${tempFile}`) const previousAsset = await checkIfAlreadyExists(params, s3)
const fs = require('fs') if (previousAsset.isAlreadyInBucket) {
const stl = fs.readFileSync(`/tmp/${tempFile}/output.stl`, { console.log('already in bucket')
encoding: 'base64',
})
console.log('encoded stl', stl)
const response = { const response = {
statusCode: 200, statusCode: 200,
headers: { body: JSON.stringify({
'content-type': 'application/stl', url: getObjectUrl({ ...params }, s3, tk),
}, consoleMessage: previousAsset.consoleMessage,
body: stl, }),
isBase64Encoded: true,
} }
console.log('callback fired')
callback(null, response) callback(null, response)
return
} }
const { file } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await stlExport({ file })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key: stlKey,
s3,
params,
tk,
})
} }
module.exports = { module.exports = {
health: middy(health).use(cors()),
stl: middy(stl).use(cors()), stl: middy(stl).use(cors()),
preview: middy(preview).use(cors()), preview: middy(loggerWrap(preview)).use(cors()),
preflightOptions,
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,11 @@
"author": "Kurt Hutten <kurt@kurthutten.com>", "author": "Kurt Hutten <kurt@kurthutten.com>",
"license": "", "license": "",
"dependencies": { "dependencies": {
"aws-sdk": "^2.907.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"middy": "^0.36.0", "middy": "^0.36.0",
"nanoid": "^3.1.20" "nanoid": "^3.1.20",
"timekeeper": "2.2.0"
}, },
"devDependencies": { "devDependencies": {
"aws-lambda-ric": "^1.0.0" "aws-lambda-ric": "^1.0.0"

View File

@@ -1,6 +1,12 @@
const { makeFile, runCommand } = require('../common/utils') const { makeFile, runCommand } = require('../common/utils')
const { nanoid } = require('nanoid') const { nanoid } = require('nanoid')
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad`
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
const cleanOpenScadError = (error) =>
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
module.exports.runScad = async ({ module.exports.runScad = async ({
file, file,
settings: { settings: {
@@ -16,27 +22,43 @@ module.exports.runScad = async ({
const { x: rx, y: ry, z: rz } = rotation const { x: rx, y: ry, z: rz } = rotation
const { x: px, y: py, z: pz } = position const { x: px, y: py, z: pz } = position
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}` const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
const command = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad -o /tmp/${tempFile}/output.png ${cameraArg} --imgsize=${x},${y} --colorscheme DeepOcean /tmp/${tempFile}/main.scad` const fullPath = `/tmp/${tempFile}/output.png`
const command = [
OPENSCAD_COMMON,
`-o ${fullPath}`,
cameraArg,
`--imgsize=${x},${y}`,
`--colorscheme CadHub`,
`/tmp/${tempFile}/main.scad`,
`&& gzip ${fullPath}`,
].join(' ')
console.log('command', command) console.log('command', command)
try { try {
const result = await runCommand(command, 15000) const consoleMessage = await runCommand(command, 15000)
return { result, tempFile } return { consoleMessage, fullPath }
} catch (error) { } catch (dirtyError) {
return { error, tempFile } const error = cleanOpenScadError(dirtyError)
return { error }
} }
} }
module.exports.stlExport = async ({ file } = {}) => { module.exports.stlExport = async ({ file } = {}) => {
const tempFile = await makeFile(file, '.scad', nanoid) const tempFile = await makeFile(file, '.scad', nanoid)
const fullPath = `/tmp/${tempFile}/output.stl`
const command = [
OPENSCAD_COMMON,
`--export-format=binstl`,
`-o ${fullPath}`,
`/tmp/${tempFile}/main.scad`,
`&& gzip ${fullPath}`,
].join(' ')
try { try {
const result = await runCommand( // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
`openscad -o /tmp/${tempFile}/output.stl /tmp/${tempFile}/main.scad`, const consoleMessage = await runCommand(command, 60000)
300000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation return { consoleMessage, fullPath }
)
return { result, tempFile }
} catch (error) { } catch (error) {
return { error, tempFile } return { error, fullPath }
} }
} }

View File

@@ -1,386 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
mime-types@~2.1.24:
version "2.1.29"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2"
integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
dependencies:
mime-db "1.46.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

1799
app/api/src/docker/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,60 +24,30 @@ provider:
file: ./cadquery/Dockerfile file: ./cadquery/Dockerfile
apiGateway: apiGateway:
metrics: true metrics: true
binaryMediaTypes:
# we need to allow binary types to be able to send back images and stls, but it would be better to be more specific
# ie image/png etc. as */* treats everything as binary including the json body as the input the lambdas
# which mean we need to decode the input bode from base64, but the images break with anything other than */* :(
- '*/*'
# you can overwrite defaults here # you can overwrite defaults here
# stage: dev # stage: dev
# region: us-east-1 # region: us-east-1
# you can add statements to the Lambda function's IAM Role here # you can add statements to the Lambda function's IAM Role here
# iamRoleStatements: iam:
# - Effect: "Allow" role:
# Action: statements:
# - "s3:ListBucket" - Effect: "Allow"
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } Action:
# - Effect: "Allow" - "s3:GetObject"
# Action: Resource: "arn:aws:s3:::cad-preview-bucket-prod-001/*"
# - "s3:PutObject" - Effect: "Allow"
# Resource: Action:
# Fn::Join: - "s3:PutObject"
# - "" Resource: "arn:aws:s3:::cad-preview-bucket-prod-001/*"
# - - "arn:aws:s3:::" # Dev bucket is cad-preview-bucket-dev-001/*"
# - "Ref" : "ServerlessDeploymentBucket"
# - "/*"
# you can define service wide environment variables here # you can define service wide environment variables here
# environment: # environment:
# variable1: value1 # variable1: value1
functions: functions:
# see preflightoptions comment in openscad.js
preflightopenscadpreview:
image:
name: openscadimage
command:
- openscad.preflightOptions
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/preview
method: options
preflightopenscadstl:
image:
name: openscadimage
command:
- openscad.preflightOptions
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/stl
method: options
openscadpreview: openscadpreview:
image: image:
name: openscadimage name: openscadimage
@@ -89,7 +59,10 @@ functions:
- http: - http:
path: openscad/preview path: openscad/preview
method: post method: post
timeout: 15 cors: true
timeout: 25
environment:
BUCKET: cad-preview-bucket-prod-001
openscadstl: openscadstl:
image: image:
name: openscadimage name: openscadimage
@@ -101,19 +74,10 @@ functions:
- http: - http:
path: openscad/stl path: openscad/stl
method: post method: post
cors: true
timeout: 30 timeout: 30
environment:
preflightcadquerystl: BUCKET: cad-preview-bucket-prod-001
image:
name: cadqueryimage
command:
- cadquery.preflightOptions
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: cadquery/stl
method: options
cadquerystl: cadquerystl:
image: image:
name: cadqueryimage name: cadqueryimage
@@ -125,7 +89,10 @@ functions:
- http: - http:
path: cadquery/stl path: cadquery/stl
method: post method: post
cors: true
timeout: 30 timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
# The following are a few example events you can configure # The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events # NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details # Check the event documentation for details
@@ -177,3 +144,23 @@ functions:
# NewOutput: # NewOutput:
# Description: "Description for the output" # Description: "Description for the output"
# Value: "Some output value" # Value: "Some output value"
resources:
Resources:
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'

View File

@@ -1,75 +0,0 @@
/* for local development
Install and run smee (point at this function)
```
yarn global add smee-client
smee --url https://smee.io/3zgDJiGO8TW7nvf --path /.netlify/functions/event_handler --port 8910
```
*/
import { createHmac } from 'crypto'
import { App } from '@octokit/app'
import type { Endpoints } from '@octokit/types'
import type { PullRequestEvent } from '@octokit/webhooks-types'
const app = new App({
privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
appId: process.env.GITHUB_APP_ID,
webhooks: {
secret: process.env.GITHUB_APP_SECRET,
},
})
const signRequestBody = (secret: string, body: string): string =>
'sha256=' + createHmac('sha256', secret).update(body, 'utf-8').digest('hex')
const writePullRequestComment = async ({
event,
message,
}: {
event: PullRequestEvent
message: string
}): Promise<
Endpoints['POST /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']
> => {
const octokit = await app.getInstallationOctokit(event.installation.id)
return octokit.request(
'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
{
owner: event.repository.owner.login,
repo: event.repository.name,
issue_number: event.number,
body: message,
}
)
}
export const handler = async (req: {
body: string
headers: {
'x-hub-signature-256': string
'x-github-event': string
}
}) => {
const theirSignature = req.headers['x-hub-signature-256']
const ourSignature = signRequestBody(process.env.GITHUB_APP_SECRET, req.body)
if (theirSignature !== ourSignature) {
return {
statusCode: 401,
body: 'Bad signature',
}
}
const eventType = req.headers['x-github-event']
if (eventType !== 'pull_request') {
return { statusCode: 200 }
}
const event: PullRequestEvent = JSON.parse(req.body)
if (['reopened', 'opened'].includes(event.action)) {
await writePullRequestComment({
event,
message: 'Salutations, what a fine PR you have here.',
})
}
return {
statusCode: 200,
}
}

View File

@@ -3,6 +3,7 @@ import {
makeMergedSchema, makeMergedSchema,
makeServices, makeServices,
} from '@redwoodjs/api' } from '@redwoodjs/api'
import { createSentryApolloPlugin } from 'src/lib/sentry'
import schemas from 'src/graphql/**/*.{js,ts}' import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}' import services from 'src/services/**/*.{js,ts}'
@@ -16,6 +17,7 @@ export const handler = createGraphQLHandler({
schemas, schemas,
services: makeServices({ services }), services: makeServices({ services }),
}), }),
plugins: [createSentryApolloPlugin()],
onException: () => { onException: () => {
// Disconnect from your database with an unhandled exception. // Disconnect from your database with an unhandled exception.
db.$disconnect() db.$disconnect()

View File

@@ -1,8 +1,11 @@
import { createUserInsecure } from 'src/services/users/users.js' import { createUserInsecure } from 'src/services/users/users'
import { db } from 'src/lib/db' import { db } from 'src/lib/db'
import { sentryWrapper } from 'src/lib/sentry'
import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers' import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers'
import 'graphql-tag'
import { sendMail } from 'src/lib/sendmail'
export const handler = async (req, _context) => { const unWrappedHandler = async (req, _context) => {
const body = JSON.parse(req.body) const body = JSON.parse(req.body)
console.log(body) console.log(body)
console.log(_context) console.log(_context)
@@ -54,7 +57,7 @@ export const handler = async (req, _context) => {
const user = body.user const user = body.user
const email = user.email const email = user.email
let roles = [] const roles = []
if (eventType === 'signup') { if (eventType === 'signup') {
roles.push('user') roles.push('user')
@@ -64,13 +67,53 @@ export const handler = async (req, _context) => {
}) })
const userNameSeed = enforceAlphaNumeric(user?.user_metadata?.userName) const userNameSeed = enforceAlphaNumeric(user?.user_metadata?.userName)
const userName = await generateUniqueString(userNameSeed, isUniqueCallback) // TODO maybe come up with a better default userName? const userName = await generateUniqueString(userNameSeed, isUniqueCallback) // TODO maybe come up with a better default userName?
const name = user?.user_metadata?.full_name
const input = { const input = {
email, email,
userName, userName,
name: user?.user_metadata?.full_name, name,
id: user.id, id: user.id,
} }
await createUserInsecure({ input }) await createUserInsecure({ input })
const kurtNotification = sendMail({
to: 'k.hutten@protonmail.ch',
from: {
address: 'news@mail.cadhub.xyz',
name: 'CadHub',
},
subject: `New Cadhub User`,
text: JSON.stringify(input, null, 2),
})
const welcomeMsg = sendMail({
to: email,
from: {
address: 'news@mail.cadhub.xyz',
name: 'CadHub',
},
subject: `${name} - Some things you should know about CadHub`,
text: `Hi, My name's Kurt.
I started CadHub because I wanted a community hub for people who like CodeCAD as much of I do, you should know that the development of CadHub is very much a community effort as well and if you want get involved the discord is the best place to start https://discord.gg/SD7zFRNjGH.
Long term I hope that CadHub will help push CodeCad as a paradigm forward, as there are clear benefits such as: CI/CD for parts, GIT based workflow and CodeCAD parts are normally much more robust to changes to parametric variables because the author can add logic to accommodate big changes where as GUI-CAD usually relies on blackbox heuristics and is more brittle. Sorry I'm getting into the weeds, if you want to read more on the paradigm see our blog https://learn.cadhub.xyz/.
One very easy way to help out is to simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
Hit me up anytime for questions or concerns.
Cheers,
Kurt.
k.hutten@protonmail.ch
https://twitter.com/IrevDev
irevdev#1888 - discord
`,
})
try {
await Promise.all([kurtNotification, welcomeMsg])
} catch (e) {
console.log('Problem sending emails', e)
}
return { return {
statusCode: 200, statusCode: 200,
@@ -82,3 +125,5 @@ export const handler = async (req, _context) => {
} }
} }
} }
export const handler = sentryWrapper(unWrappedHandler)

View File

@@ -0,0 +1,39 @@
export const schema = gql`
type ProjectReaction {
id: String!
emote: String!
user: User!
userId: String!
project: Project!
projectId: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
projectReactions: [ProjectReaction!]!
projectReaction(id: String!): ProjectReaction
projectReactionsByProjectId(projectId: String!): [ProjectReaction!]!
}
input ToggleProjectReactionInput {
emote: String!
userId: String!
projectId: String!
}
input UpdateProjectReactionInput {
emote: String
userId: String
projectId: String
}
type Mutation {
toggleProjectReaction(input: ToggleProjectReactionInput!): ProjectReaction!
updateProjectReaction(
id: String!
input: UpdateProjectReactionInput!
): ProjectReaction!
deleteProjectReaction(id: String!): ProjectReaction!
}
`

View File

@@ -4,8 +4,8 @@ export const schema = gql`
text: String! text: String!
user: User! user: User!
userId: String! userId: String!
part: Part! project: Project!
partId: String! projectId: String!
createdAt: DateTime! createdAt: DateTime!
updatedAt: DateTime! updatedAt: DateTime!
} }
@@ -18,13 +18,13 @@ export const schema = gql`
input CreateCommentInput { input CreateCommentInput {
text: String! text: String!
userId: String! userId: String!
partId: String! projectId: String!
} }
input UpdateCommentInput { input UpdateCommentInput {
text: String text: String
userId: String userId: String
partId: String projectId: String
} }
type Mutation { type Mutation {

View File

@@ -0,0 +1,20 @@
export const schema = gql`
type Envelope {
from: String
to: [String!]!
}
type EmailResponse {
accepted: [String!]!
rejected: [String!]!
}
input Email {
subject: String!
body: String!
}
type Mutation {
sendAllUsersEmail(input: Email!): EmailResponse!
}
`

View File

@@ -1,39 +0,0 @@
export const schema = gql`
type PartReaction {
id: String!
emote: String!
user: User!
userId: String!
part: Part!
partId: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
partReactions: [PartReaction!]!
partReaction(id: String!): PartReaction
partReactionsByPartId(partId: String!): [PartReaction!]!
}
input TogglePartReactionInput {
emote: String!
userId: String!
partId: String!
}
input UpdatePartReactionInput {
emote: String
userId: String
partId: String
}
type Mutation {
togglePartReaction(input: TogglePartReactionInput!): PartReaction!
updatePartReaction(
id: String!
input: UpdatePartReactionInput!
): PartReaction!
deletePartReaction(id: String!): PartReaction!
}
`

View File

@@ -1,45 +0,0 @@
export const schema = gql`
type Part {
id: String!
title: String!
description: String
code: String
mainImage: String
createdAt: DateTime!
updatedAt: DateTime!
deleted: Boolean!
user: User!
userId: String!
Comment: [Comment]!
Reaction(userId: String): [PartReaction]!
}
type Query {
parts(userName: String): [Part!]!
part(id: String!): Part
partByUserAndTitle(userName: String!, partTitle: String!): Part
}
input CreatePartInput {
title: String!
description: String
code: String
mainImage: String
userId: String!
}
input UpdatePartInput {
title: String
description: String
code: String
mainImage: String
userId: String
}
type Mutation {
createPart(input: CreatePartInput!): Part!
forkPart(input: CreatePartInput!): Part!
updatePart(id: String!, input: UpdatePartInput!): Part!
deletePart(id: String!): Part!
}
`

View File

@@ -0,0 +1,58 @@
export const schema = gql`
type Project {
id: String!
title: String!
description: String
code: String
mainImage: String
createdAt: DateTime!
updatedAt: DateTime!
user: User!
userId: String!
deleted: Boolean!
cadPackage: CadPackage!
socialCard: SocialCard
Comment: [Comment]!
Reaction(userId: String): [ProjectReaction]!
}
enum CadPackage {
openscad
cadquery
}
type Query {
projects(userName: String): [Project!]!
project(id: String!): Project
projectByUserAndTitle(userName: String!, projectTitle: String!): Project
}
input CreateProjectInput {
title: String
description: String
code: String
mainImage: String
userId: String!
cadPackage: CadPackage!
}
input UpdateProjectInput {
title: String
description: String
code: String
mainImage: String
userId: String
}
type Mutation {
createProject(input: CreateProjectInput!): Project!
forkProject(input: CreateProjectInput!): Project!
updateProject(id: String!, input: UpdateProjectInput!): Project!
updateProjectImages(
id: String!
mainImage64: String
socialCard64: String
): Project!
deleteProject(id: String!): Project!
}
`

View File

@@ -0,0 +1,16 @@
export const schema = gql`
type SocialCard {
id: String!
projectId: String!
project: Project!
createdAt: DateTime!
updatedAt: DateTime!
url: String
outOfDate: Boolean!
}
type Query {
socialCards: [SocialCard!]!
socialCard(id: String!): SocialCard
}
`

View File

@@ -8,9 +8,9 @@ export const schema = gql`
updatedAt: DateTime! updatedAt: DateTime!
image: String image: String
bio: String bio: String
Parts: [Part]! Projects: [Project]!
Part(partTitle: String): Part Project(projectTitle: String): Project
Reaction: [PartReaction]! Reaction: [ProjectReaction]!
Comment: [Comment]! Comment: [Comment]!
SubjectAccessRequest: [SubjectAccessRequest]! SubjectAccessRequest: [SubjectAccessRequest]!
} }

View File

@@ -121,7 +121,8 @@ export const getCurrentUser = async (decoded, { _token, _type }) => {
* requireAuth({ role: ['editor', 'author'] }) * requireAuth({ role: ['editor', 'author'] })
* requireAuth({ role: ['publisher'] }) * requireAuth({ role: ['publisher'] })
*/ */
export const requireAuth = ({ role } = {}) => { export const requireAuth = ({ role }: { role?: string | string[] } = {}) => {
console.log(context.currentUser)
if (!context.currentUser) { if (!context.currentUser) {
throw new AuthenticationError("You don't have permission to do that.") throw new AuthenticationError("You don't have permission to do that.")
} }

View File

@@ -0,0 +1,45 @@
import {
v2 as cloudinary,
UploadApiResponse,
UpdateApiOptions,
} from 'cloudinary'
cloudinary.config({
cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})
interface UploadImageArgs {
image64: string
uploadPreset?: string
publicId?: string
invalidate: boolean
}
export const uploadImage = async ({
image64,
uploadPreset = 'CadHub_project_images',
publicId,
invalidate = true,
}: UploadImageArgs): Promise<UploadApiResponse> => {
const options: UpdateApiOptions = { upload_preset: uploadPreset, invalidate }
if (publicId) {
options.public_id = publicId
}
return new Promise((resolve, reject) => {
cloudinary.uploader.upload(image64, options, (error, result) => {
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
export const makeSocialPublicIdServer = (
userName: string,
projectTitle: string
): string => `u-${userName}-slash-p-${projectTitle}`

View File

@@ -1,44 +0,0 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import { db } from 'src/lib/db'
export const requireOwnership = async ({ userId, userName, partId } = {}) => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!context.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!userId && !userName && !partId) {
throw new ForbiddenError("You don't have access to do that.")
}
if (context.currentUser.roles?.includes('admin')) {
return
}
const netlifyUserId = context.currentUser?.sub
if (userId && userId !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
if (userName) {
const user = await db.user.findUnique({
where: { userName },
})
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
if (partId) {
const user = await db.part
.findUnique({
where: { id: partId },
})
.user()
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
}

94
app/api/src/lib/owner.ts Normal file
View File

@@ -0,0 +1,94 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import type { Project } from '@prisma/client'
import { db } from 'src/lib/db'
export const requireOwnership = async ({
userId,
userName,
projectId,
sub,
}: {
userId?: string
userName?: string
projectId?: string
sub?: string
} = {}) => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!(context?.currentUser || sub)) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!userId && !userName && !projectId) {
throw new ForbiddenError("You don't have access to do that.")
}
if (context.currentUser.roles?.includes('admin')) {
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
throw new ForbiddenError("That's a local admin ONLY.")
}
return
}
const netlifyUserId = context?.currentUser?.sub || sub
if (userId && userId !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
if (userName) {
const user = await db.user.findUnique({
where: { userName },
})
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
if (projectId) {
const user = await db.project
.findUnique({
where: { id: projectId },
})
.user()
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
}
export const requireProjectOwnership = async ({
projectId,
}: {
userId?: string
userName?: string
projectId?: string
sub?: string
} = {}): Promise<Project> => {
// IMPORTANT, don't forget to await this function, as it will only block
// unwanted db actions if it has time to look up resources in the db.
if (!context?.currentUser) {
throw new AuthenticationError("You don't have permission to do that.")
}
if (!projectId) {
throw new ForbiddenError("You don't have access to do that.")
}
const netlifyUserId = context?.currentUser?.sub
if (projectId || context.currentUser.roles?.includes('admin')) {
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
throw new ForbiddenError("That's a local admin ONLY.")
}
const project = await db.project.findUnique({
where: { id: projectId },
})
const hasPermission =
(project && project?.userId === netlifyUserId) ||
context.currentUser.roles?.includes('admin')
if (!hasPermission) {
throw new ForbiddenError("You don't own this resource.")
}
return project
}
}

View File

@@ -0,0 +1,63 @@
import nodemailer, { SendMailOptions } from 'nodemailer'
export interface SendMailArgs {
to: string
from: SendMailOptions['from']
subject: string
text: string
}
interface SuccessResult {
accepted: string[]
rejected: string[]
envelopeTime: number
messageTime: number
messageSize: number
response: string
envelope: {
from: string | false
to: string[]
}
messageId: string
}
export function sendMail({
to,
from,
subject,
text,
}: SendMailArgs): Promise<SuccessResult> {
const transporter = nodemailer.createTransport({
host: 'smtp.mailgun.org',
port: 587,
secure: false,
tls: {
ciphers: 'SSLv3',
},
auth: {
user: 'postmaster@mail.cadhub.xyz',
pass: process.env.EMAIL_PASSWORD,
},
})
console.log({ to, from, subject, text })
const emailPromise = new Promise((resolve, reject) => {
transporter.sendMail(
{
from,
to,
subject,
text,
},
(error, info) => {
if (error) {
reject(error)
} else {
resolve(info)
}
}
)
}) as any as Promise<SuccessResult>
return emailPromise
}

105
app/api/src/lib/sentry.ts Normal file
View File

@@ -0,0 +1,105 @@
import * as Sentry from '@sentry/node'
import { context, Config, ApolloError } from '@redwoodjs/api'
let sentryInitialized = false
if (process.env.SENTRY_DSN && !sentryInitialized) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.CONTEXT,
release: process.env.COMMIT_REF,
})
sentryInitialized = true
}
async function reportError(error) {
if (!sentryInitialized) return
// If you do have authentication set up, we can add
// some user data to help debug issues
// if (context.currentUser) {
// Sentry.configureScope((scope) => {
// scope.setUser({
// id: context?.currentUser?.id,
// email: context?.currentUser?.email,
// })
// })
// }
if (typeof error === 'string') {
Sentry.captureMessage(error)
} else {
Sentry.captureException(error)
}
await Sentry.flush()
}
export const sentryWrapper = (handler) => async (event, lambdaContext) => {
lambdaContext.callbackWaitsForEmptyEventLoop = false
try {
return await new Promise((resolve, reject) => {
const callback = (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
}
const resp = handler(event, lambdaContext, callback)
if (resp?.then) {
resp.then(resolve, reject)
}
})
} catch (e) {
// This catches both sync errors & promise
// rejections, because we 'await' on the handler
await reportError(e)
throw e
}
}
export const createSentryApolloPlugin: Config['plugins'][number] = () => ({
requestDidStart: () => {
return {
didEncounterErrors(ctx) {
// If we couldn't parse the operation, don't
// do anything here
if (!ctx.operation) {
return
}
for (const err of ctx.errors) {
// Only report internal server errors,
// all errors extending ApolloError should be user-facing
if (err instanceof ApolloError) {
continue
}
// Add scoped report details and send to Sentry
Sentry.withScope((scope) => {
// Annotate whether failing operation was query/mutation/subscription
scope.setTag('kind', ctx.operation.operation)
// Log query and variables as extras (make sure to strip out sensitive data!)
scope.setExtra('query', ctx.request.query)
scope.setExtra('variables', ctx.request.variables)
if (err.path) {
// We can also add the path as breadcrumb
scope.addBreadcrumb({
category: 'query-path',
message: err.path.join(' > '),
level: Sentry.Severity.Debug,
})
}
const transactionId =
ctx.request.http.headers.get('x-transaction-id')
if (transactionId) {
scope.setTransaction(transactionId)
}
Sentry.captureException(err)
})
}
},
}
},
})

View File

@@ -1,9 +0,0 @@
/*
import { comments } from './comments'
*/
describe('comments', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -33,6 +33,6 @@ export const deleteComment = ({ id }) => {
export const Comment = { export const Comment = {
user: (_obj, { root }) => user: (_obj, { root }) =>
db.comment.findUnique({ where: { id: root.id } }).user(), db.comment.findUnique({ where: { id: root.id } }).user(),
part: (_obj, { root }) => project: (_obj, { root }) =>
db.comment.findUnique({ where: { id: root.id } }).part(), db.comment.findUnique({ where: { id: root.id } }).project(),
} }

View File

@@ -0,0 +1,45 @@
import { requireAuth } from 'src/lib/auth'
import { sendMail } from 'src/lib/sendmail'
import type { SendMailArgs } from 'src/lib/sendmail'
import { users } from 'src/services/users/users'
export const sendAllUsersEmail = async ({ input: { body, subject } }) => {
requireAuth({ role: 'admin' })
const from = {
address: 'news@mail.cadhub.xyz',
name: 'CadHub',
}
const emails: SendMailArgs[] = (await users()).map(({ email }) => ({
to: email,
from,
subject,
text: body,
}))
const emailPromises = emails.map((email) => sendMail(email))
const accepted = []
const rejected = []
const result = await Promise.allSettled(emailPromises)
result.forEach((result) => {
if (result.status === 'fulfilled') {
accepted.push(result.value.accepted[0])
} else {
rejected.push(result.reason)
}
})
await sendMail({
to: 'k.hutten@protonmail.ch',
from,
subject: `All users email report`,
text: JSON.stringify(
{
accepted,
rejected,
originalEmailList: emails,
},
null,
2
),
})
return { accepted, rejected }
}

View File

@@ -1,4 +1,6 @@
import { v2 as cloudinary } from 'cloudinary' import { v2 as cloudinary } from 'cloudinary'
import humanId from 'human-id'
cloudinary.config({ cloudinary.config({
cloud_name: 'irevdev', cloud_name: 'irevdev',
api_key: process.env.CLOUDINARY_API_KEY, api_key: process.env.CLOUDINARY_API_KEY,
@@ -20,7 +22,7 @@ export const foreignKeyReplacement = (input) => {
} }
export const enforceAlphaNumeric = (string) => export const enforceAlphaNumeric = (string) =>
string.replace(/([^a-zA-Z\d_:])/g, '-') (string || '').replace(/([^a-zA-Z\d_:])/g, '-')
export const generateUniqueString = async ( export const generateUniqueString = async (
seed, seed,
@@ -36,6 +38,26 @@ export const generateUniqueString = async (
return generateUniqueString(newSeed, isUniqueCallback, count) return generateUniqueString(newSeed, isUniqueCallback, count)
} }
export const generateUniqueStringWithoutSeed = async (
isUniqueCallback: (seed: string) => Promise<any>,
count = 0
) => {
const seed = humanId({
separator: '-',
capitalize: false,
})
const isUnique = !(await isUniqueCallback(seed))
if (isUnique) {
return seed
}
count += 1
if (count > 100) {
console.log('trouble finding unique')
return `very-unique-${seed}`.slice(0, 10)
}
return generateUniqueStringWithoutSeed(isUniqueCallback, count)
}
export const destroyImage = ({ publicId }) => export const destroyImage = ({ publicId }) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
cloudinary.uploader.destroy(publicId, (error, result) => { cloudinary.uploader.destroy(publicId, (error, result) => {

View File

@@ -1,9 +0,0 @@
/*
import { partReactions } from './partReactions'
*/
describe('partReactions', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -1,113 +0,0 @@
import { db } from 'src/lib/db'
import {
foreignKeyReplacement,
enforceAlphaNumeric,
generateUniqueString,
destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
export const parts = ({ userName }) => {
if (!userName) {
return db.part.findMany({ where: { deleted: false } })
}
return db.part.findMany({
where: {
deleted: false,
user: {
userName,
},
},
})
}
export const part = ({ id }) => {
return db.part.findUnique({
where: { id },
})
}
export const partByUserAndTitle = async ({ userName, partTitle }) => {
const user = await db.user.findUnique({
where: {
userName,
},
})
return db.part.findUnique({
where: {
title_userId: {
title: partTitle,
userId: user.id,
},
},
})
}
export const createPart = async ({ input }) => {
requireAuth()
return db.part.create({
data: foreignKeyReplacement(input),
})
}
export const forkPart = async ({ input }) => {
// Only difference between create and fork part is that fork part will generate a unique title
// (for the user) if there is a conflict
const isUniqueCallback = async (seed) =>
db.part.findUnique({
where: {
title_userId: {
title: seed,
userId: input.userId,
},
},
})
const title = await generateUniqueString(input.title, isUniqueCallback)
// TODO change the description to `forked from userName/partName ${rest of description}`
return db.part.create({
data: foreignKeyReplacement({ ...input, title }),
})
}
export const updatePart = async ({ id, input }) => {
requireAuth()
await requireOwnership({ partId: id })
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
const originalPart = await db.part.findUnique({ where: { id } })
const imageToDestroy =
originalPart.mainImage !== input.mainImage && originalPart.mainImage
const update = await db.part.update({
data: foreignKeyReplacement(input),
where: { id },
})
if (imageToDestroy) {
console.log(`image destroyed, publicId: ${imageToDestroy}, partId: ${id}`)
// destroy after the db has been updated
destroyImage({ publicId: imageToDestroy })
}
return update
}
export const deletePart = async ({ id }) => {
requireAuth()
await requireOwnership({ partId: id })
return db.part.update({
data: {
deleted: true,
},
where: { id },
})
}
export const Part = {
user: (_obj, { root }) =>
db.part.findUnique({ where: { id: root.id } }).user(),
Comment: (_obj, { root }) =>
db.part.findUnique({ where: { id: root.id } }).Comment(),
Reaction: (_obj, { root }) =>
db.part
.findUnique({ where: { id: root.id } })
.Reaction({ where: { userId: _obj.userId } }),
}

View File

@@ -1,9 +0,0 @@
/*
import { parts } from './parts'
*/
describe('parts', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -5,24 +5,24 @@ import { requireOwnership } from 'src/lib/owner'
import { db } from 'src/lib/db' import { db } from 'src/lib/db'
import { foreignKeyReplacement } from 'src/services/helpers' import { foreignKeyReplacement } from 'src/services/helpers'
export const partReactions = () => { export const projectReactions = () => {
return db.partReaction.findMany() return db.projectReaction.findMany()
} }
export const partReaction = ({ id }) => { export const projectReaction = ({ id }) => {
return db.partReaction.findUnique({ return db.projectReaction.findUnique({
where: { id }, where: { id },
}) })
} }
export const partReactionsByPartId = ({ partId }) => { export const projectReactionsByProjectId = ({ projectId }) => {
return db.partReaction.findMany({ return db.projectReaction.findMany({
where: { partId: partId }, where: { projectId },
}) })
} }
export const togglePartReaction = async ({ input }) => { export const toggleProjectReaction = async ({ input }) => {
// if write fails emote_userId_partId @@unique constraint, then delete it instead // if write fails emote_userId_projectId @@unique constraint, then delete it instead
requireAuth() requireAuth()
await requireOwnership({ userId: input?.userId }) await requireOwnership({ userId: input?.userId })
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
@@ -36,33 +36,33 @@ export const togglePartReaction = async ({ input }) => {
let dbPromise let dbPromise
const inputClone = { ...input } // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now const inputClone = { ...input } // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
try { try {
dbPromise = await db.partReaction.create({ dbPromise = await db.projectReaction.create({
data: foreignKeyReplacement(input), data: foreignKeyReplacement(input),
}) })
} catch (e) { } catch (e) {
dbPromise = db.partReaction.delete({ dbPromise = db.projectReaction.delete({
where: { emote_userId_partId: inputClone }, where: { emote_userId_projectId: inputClone },
}) })
} }
return dbPromise return dbPromise
} }
export const updatePartReaction = ({ id, input }) => { export const updateProjectReaction = ({ id, input }) => {
return db.partReaction.update({ return db.projectReaction.update({
data: foreignKeyReplacement(input), data: foreignKeyReplacement(input),
where: { id }, where: { id },
}) })
} }
export const deletePartReaction = ({ id }) => { export const deleteProjectReaction = ({ id }) => {
return db.partReaction.delete({ return db.projectReaction.delete({
where: { id }, where: { id },
}) })
} }
export const PartReaction = { export const ProjectReaction = {
user: (_obj, { root }) => user: (_obj, { root }) =>
db.partReaction.findUnique({ where: { id: root.id } }).user(), db.projectReaction.findUnique({ where: { id: root.id } }).user(),
part: (_obj, { root }) => project: (_obj, { root }) =>
db.partReaction.findUnique({ where: { id: root.id } }).part(), db.projectReaction.findUnique({ where: { id: root.id } }).project(),
} }

View File

@@ -0,0 +1,266 @@
import type { Prisma, Project as ProjectType } from '@prisma/client'
import type { ResolverArgs } from '@redwoodjs/api'
import { uploadImage, makeSocialPublicIdServer } from 'src/lib/cloudinary'
import { db } from 'src/lib/db'
import {
foreignKeyReplacement,
enforceAlphaNumeric,
generateUniqueString,
generateUniqueStringWithoutSeed,
destroyImage,
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
import { socialCard } from '../socialCards/socialCards'
export const projects = ({ userName }) => {
if (!userName) {
return db.project.findMany({ where: { deleted: false } })
}
return db.project.findMany({
where: {
deleted: false,
user: {
userName,
},
},
})
}
export const project = ({ id }: Prisma.ProjectWhereUniqueInput) => {
return db.project.findUnique({
where: { id },
})
}
export const projectByUserAndTitle = async ({ userName, projectTitle }) => {
const user = await db.user.findUnique({
where: {
userName,
},
})
return db.project.findUnique({
where: {
title_userId: {
title: projectTitle,
userId: user.id,
},
},
})
}
const isUniqueProjectTitle = (userId: string) => async (seed: string) =>
db.project.findUnique({
where: {
title_userId: {
title: seed,
userId,
},
},
})
interface CreateProjectArgs {
input: Prisma.ProjectCreateArgs['data']
}
export const createProject = async ({ input }: CreateProjectArgs) => {
requireAuth()
console.log(input.userId)
const isUniqueCallback = isUniqueProjectTitle(input.userId)
let title = input.title
if (!title) {
title = await generateUniqueStringWithoutSeed(isUniqueCallback)
}
return db.project.create({
data: foreignKeyReplacement({
...input,
title,
}),
})
}
export const forkProject = async ({ input }) => {
// Only difference between create and fork project is that fork project will generate a unique title
// (for the user) if there is a conflict
const isUniqueCallback = isUniqueProjectTitle(input.userId)
const title = await generateUniqueString(input.title, isUniqueCallback)
// TODO change the description to `forked from userName/projectName ${rest of description}`
return db.project.create({
data: foreignKeyReplacement({ ...input, title }),
})
}
interface UpdateProjectArgs extends Prisma.ProjectWhereUniqueInput {
input: Prisma.ProjectUpdateInput
}
export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
const checkSocialCardValidity = async (
projectId: string,
input: UpdateProjectArgs['input'],
oldProject: ProjectType
) => {
const titleChange = input.title && input.title !== oldProject.title
const descriptionChange =
input.description && input.description !== oldProject.description
if (titleChange || descriptionChange) {
const socialCard = await db.socialCard.findUnique({where: {projectId}})
if (socialCard) {
return db.socialCard.update({
data: { outOfDate: true },
where: { id: socialCard.id },
})
}
}
}
requireAuth()
const originalProject = await requireProjectOwnership({ projectId: id })
console.log('yooooo', originalProject)
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
const socialCardPromise = checkSocialCardValidity(id, input, originalProject)
const imageToDestroy =
originalProject.mainImage !== input.mainImage &&
input.mainImage &&
originalProject.mainImage
const update = await db.project.update({
data: foreignKeyReplacement(input),
where: { id },
})
if (imageToDestroy) {
console.log(
`image destroyed, publicId: ${imageToDestroy}, projectId: ${id}, replacing image is ${input.mainImage}`
)
// destroy after the db has been updated
await destroyImage({ publicId: imageToDestroy })
}
await socialCardPromise
return update
}
export const updateProjectImages = async ({
id,
mainImage64,
socialCard64,
}: {
id: string
mainImage64?: string
socialCard64?: string
}): Promise<ProjectType> => {
requireAuth()
const project = await requireProjectOwnership({ projectId: id })
const replaceSocialCard = async () => {
if (!socialCard64) {
return
}
let publicId = ''
let socialCardId = ''
try {
;({ id: socialCardId, url: publicId } = await db.socialCard.findUnique({
where: { projectId: id },
}))
} catch (e) {
const { userName } = await db.user.findUnique({
where: { id: project.userId },
})
publicId = makeSocialPublicIdServer(userName, project.title)
}
const imagePromise = uploadImage({
image64: socialCard64,
uploadPreset: 'CadHub_project_images',
publicId,
invalidate: true,
})
const saveOrUpdateSocialCard = () => {
const data = {
outOfDate: false,
url: publicId,
}
if (socialCardId) {
return db.socialCard.update({
data,
where: { projectId: id },
})
}
return db.socialCard.create({
data: {
...data,
project: {
connect: {
id: id,
},
},
},
})
}
const socialCardUpdatePromise = saveOrUpdateSocialCard()
const [socialCard] = await Promise.all([
socialCardUpdatePromise,
imagePromise,
])
return socialCard
}
const updateMainImage = async (): Promise<ProjectType> => {
if (!mainImage64) {
return project
}
const { public_id: mainImage } = await uploadImage({
image64: mainImage64,
uploadPreset: 'CadHub_project_images',
invalidate: true,
})
const projectPromise = db.project.update({
data: {
mainImage,
},
where: { id },
})
let imageDestroyPromise = new Promise((r) => r(null))
if (project.mainImage) {
console.log(
`image destroyed, publicId: ${project.mainImage}, projectId: ${id}, replacing image is ${mainImage}`
)
// destroy after the db has been updated
imageDestroyPromise = destroyImage({ publicId: project.mainImage })
}
const [updatedProject] = await Promise.all([
projectPromise,
imageDestroyPromise,
])
return updatedProject
}
const [updatedProject] = await Promise.all([
updateMainImage(),
replaceSocialCard(),
])
return updatedProject
}
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
requireAuth()
await requireOwnership({ projectId: id })
return db.project.update({
data: {
deleted: true,
},
where: { id },
})
}
export const Project = {
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).user(),
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).socialCard(),
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project
.findUnique({ where: { id: root.id } })
.Comment({ orderBy: { createdAt: 'desc' } }),
Reaction: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project
.findUnique({ where: { id: root.id } })
.Reaction({ where: { userId: _obj.userId } }),
}

View File

@@ -0,0 +1,25 @@
import type { Prisma } from '@prisma/client'
import type { ResolverArgs, BeforeResolverSpecType } from '@redwoodjs/api'
import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
// Used when the environment variable REDWOOD_SECURE_SERVICES=1
export const beforeResolver = (rules: BeforeResolverSpecType) => {
rules.add(requireAuth)
}
export const socialCards = () => {
return db.socialCard.findMany()
}
export const socialCard = ({ id }: Prisma.SocialCardWhereUniqueInput) => {
return db.socialCard.findUnique({
where: { id },
})
}
export const SocialCard = {
project: (_obj, { root }: ResolverArgs<ReturnType<typeof socialCard>>) =>
db.socialCard.findUnique({ where: { id: root.id } }).project(),
}

View File

@@ -1,9 +0,0 @@
/*
import { subjectAccessRequests } from './subjectAccessRequests'
*/
describe('subjectAccessRequests', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -1,9 +0,0 @@
/*
import { users } from './users'
*/
describe('users', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -51,9 +51,9 @@ export const updateUserByUserName = async ({ userName, input }) => {
`You've tried to used a protected word as you userName, try something other than ` `You've tried to used a protected word as you userName, try something other than `
) )
} }
const originalPart = await db.user.findUnique({ where: { userName } }) const originalProject = await db.user.findUnique({ where: { userName } })
const imageToDestroy = const imageToDestroy =
originalPart.image !== input.image && originalPart.image originalProject.image !== input.image && originalProject.image
const update = await db.user.update({ const update = await db.user.update({
data: input, data: input,
where: { userName }, where: { userName },
@@ -73,14 +73,14 @@ export const deleteUser = ({ id }) => {
} }
export const User = { export const User = {
Parts: (_obj, { root }) => Projects: (_obj, { root }) =>
db.user.findUnique({ where: { id: root.id } }).Part(), db.user.findUnique({ where: { id: root.id } }).Project(),
Part: (_obj, { root }) => Project: (_obj, { root }) =>
_obj.partTitle && _obj.projectTitle &&
db.part.findUnique({ db.project.findUnique({
where: { where: {
title_userId: { title_userId: {
title: _obj.partTitle, title: _obj.projectTitle,
userId: root.id, userId: root.id,
}, },
}, },

17
app/api/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"esModuleInterop": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"src/*": ["./src/*"]
},
"typeRoots": ["../node_modules/@types", "./node_modules/@types"],
"types": ["jest"]
},
"include": ["src", "../.redwood/**/*"]
}

View File

@@ -1,7 +1,5 @@
const { getConfig } = require('@redwoodjs/internal') const { getPaths } = require('@redwoodjs/internal')
const config = getConfig()
module.exports = { module.exports = {
schema: `http://${config.api.host}:${config.api.port}/graphql`, schema: getPaths().generated.schema,
} }

View File

@@ -4,7 +4,16 @@ publish = "web/dist"
functions = "api/dist/functions" functions = "api/dist/functions"
[dev] [dev]
command = "yarn rw dev" # To use [Netlify Dev](https://www.netlify.com/products/dev/),
# install netlify-cli from https://docs.netlify.com/cli/get-started/#installation
# and then use netlify link https://docs.netlify.com/cli/get-started/#link-and-unlink-sites
# to connect your local project to a site already on Netlify
# then run netlify dev and our app will be accessible on the port specified below
framework = "redwoodjs"
# Set targetPort to the [web] side port as defined in redwood.toml
targetPort = 8910
# Point your browser to this port to access your RedwoodJS app
port = 8888
[[redirects]] [[redirects]]
from = "/*" from = "/*"
@@ -13,3 +22,10 @@ functions = "api/dist/functions"
[context.deploy-preview.environment] [context.deploy-preview.environment]
CAD_LAMBDA_BASE_URL = "https://t7wdlz8ztf.execute-api.us-east-1.amazonaws.com/dev2" CAD_LAMBDA_BASE_URL = "https://t7wdlz8ztf.execute-api.us-east-1.amazonaws.com/dev2"
[[plugins]]
package = "@sentry/netlify-build-plugin"
[plugins.inputs]
sentryOrg = "kurt"
sentryProject = "kurt"

View File

@@ -8,17 +8,25 @@
}, },
"scripts": {}, "scripts": {},
"devDependencies": { "devDependencies": {
"@redwoodjs/core": "^0.31.0" "@redwoodjs/core": "^0.34.1"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "@redwoodjs/eslint-config" "extends": "@redwoodjs/eslint-config",
"rules": {
"react/no-unescaped-entities": [
"error",
{
"forbid": [
">",
"}",
"\""
]
}
]
}
}, },
"engines": { "engines": {
"node": ">=14", "node": ">=14",
"yarn": ">=1.15" "yarn": ">=1.15"
},
"resolutions": {
"react": "17.0.1",
"react-dom": "17.0.1"
} }
} }

View File

@@ -8,10 +8,24 @@
[web] [web]
port = 8910 port = 8910
apiProxyPath = "/.netlify/functions" apiProxyPath = "/.netlify/functions"
includeEnvironmentVariables = ['GOOGLE_ANALYTICS_ID', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET', 'CAD_LAMBDA_BASE_URL'] includeEnvironmentVariables = [
'GOOGLE_ANALYTICS_ID',
'CLOUDINARY_API_KEY',
'CLOUDINARY_API_SECRET',
'CAD_LAMBDA_BASE_URL',
'SENTRY_DSN',
'SENTRY_AUTH_TOKEN',
'SENTRY_ORG',
'SENTRY_PROJECT',
'EMAIL_PASSWORD'
]
# experimentalFastRefresh = true # this seems to break cascadeStudio # experimentalFastRefresh = true # this seems to break cascadeStudio
[api] [api]
port = 8911 port = 8911
schemaPath = "./api/db/schema.prisma" schemaPath = "./api/db/schema.prisma"
[browser] [browser]
open = true open = true
[experimental]
esbuild = true

View File

@@ -1,92 +1,9 @@
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.exports = (config, { env }) => { module.exports = (config, { env }) => {
config.plugins.forEach((plugin) => { config.plugins.forEach((plugin) => {
if (plugin.constructor.name === 'HtmlWebpackPlugin') { if (plugin.constructor.name === 'HtmlWebpackPlugin') {
plugin.options.favicon = './src/favicon.svg' plugin.options.favicon = './src/favicon.svg'
} else if (plugin.constructor.name === 'CopyPlugin') {
plugin.patterns.push({
from: './src/cascade/js/StandardLibraryIntellisense.ts',
to: 'js/StandardLibraryIntellisense.ts',
})
plugin.patterns.push({
from: './src/cascade/static_node_modules/opencascade.js/dist/oc.d.ts',
to: 'opencascade.d.ts',
})
plugin.patterns.push({
from: '../node_modules/three/src/Three.d.ts',
to: 'Three.d.ts',
})
plugin.patterns.push({
from: './src/cascade/fonts',
to: 'fonts',
})
plugin.patterns.push({
from: './src/cascade/textures',
to: 'textures',
})
} }
}) })
config.plugins.push(
new MonacoWebpackPlugin({
languages: ['typescript'],
features: [
'accessibilityHelp',
'anchorSelect',
'bracketMatching',
'caretOperations',
'clipboard',
'codeAction',
'codelens',
'comment',
'contextmenu',
'coreCommands',
'cursorUndo',
'documentSymbols',
'find',
'folding',
'fontZoom',
'format',
'gotoError',
'gotoLine',
'gotoSymbol',
'hover',
'inPlaceReplace',
'indentation',
'inlineHints',
'inspectTokens',
'linesOperations',
'linkedEditing',
'links',
'multicursor',
'parameterHints',
'quickCommand',
'quickHelp',
'quickOutline',
'referenceSearch',
'rename',
'smartSelect',
'snippets',
'suggest',
'toggleHighContrast',
'toggleTabFocusMode',
'transpose',
'unusualLineTerminators',
'viewportSemanticTokens',
'wordHighlighter',
'wordOperations',
'wordPartOperations',
],
})
)
config.module.rules[0].oneOf.push({
test: /opencascade\.wasm\.wasm$/,
type: 'javascript/auto',
loader: 'file-loader',
})
config.node = {
fs: 'empty',
}
return config return config
} }

View File

@@ -1,6 +1 @@
const { getConfig } = require('@redwoodjs/core') module.exports = require('@redwoodjs/testing/config/jest/api')
const config = getConfig({ type: 'jest', target: 'browser' })
config.displayName.name = 'web'
module.exports = config

View File

@@ -16,40 +16,41 @@
"@headlessui/react": "^1.0.0", "@headlessui/react": "^1.0.0",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@monaco-editor/react": "^4.0.11", "@monaco-editor/react": "^4.0.11",
"@redwoodjs/auth": "^0.31.0", "@react-three/drei": "^7.3.1",
"@redwoodjs/forms": "^0.31.0", "@react-three/fiber": "^7.0.5",
"@redwoodjs/router": "^0.31.0", "@redwoodjs/auth": "^0.34.1",
"@redwoodjs/web": "^0.31.0", "@redwoodjs/forms": "^0.34.1",
"@redwoodjs/router": "^0.34.1",
"@redwoodjs/web": "^0.34.1",
"@sentry/browser": "^6.5.1",
"@tailwindcss/aspect-ratio": "0.2.1",
"browser-fs-access": "^0.17.2",
"cloudinary-react": "^1.6.7", "cloudinary-react": "^1.6.7",
"controlkit": "^0.1.9",
"get-active-classes": "^0.0.11", "get-active-classes": "^0.0.11",
"golden-layout": "^1.5.9",
"gotrue-js": "^0.9.27", "gotrue-js": "^0.9.27",
"jquery": "^3.5.1", "html-to-image": "^1.7.0",
"monaco-editor": "^0.20.0", "lodash": "^4.17.21",
"monaco-editor-webpack-plugin": "^1.9.1",
"netlify-identity-widget": "^1.9.1", "netlify-identity-widget": "^1.9.1",
"opencascade.js": "^0.1.15", "pako": "^2.0.3",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.1", "react": "^17.0.2",
"react-dom": "^17.0.1", "react-dom": "^17.0.2",
"react-dropzone": "^11.2.1", "react-dropzone": "^11.2.1",
"react-ga": "^3.3.0", "react-ga": "^3.3.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-image-crop": "^8.6.6", "react-image-crop": "^8.6.6",
"react-mosaic-component": "^4.1.1", "react-mosaic-component": "^4.1.1",
"react-three-fiber": "^5.3.19", "react-tabs": "^3.2.2",
"rich-markdown-editor": "^11.0.2", "rich-markdown-editor": "^11.0.2",
"styled-components": "^5.2.0", "styled-components": "^5.2.0",
"three": "^0.118.3" "three": "^0.130.1"
}, },
"devDependencies": { "devDependencies": {
"postcss": "^8.2.13", "@types/lodash": "^4.14.170",
"autoprefixer": "^10.2.5", "autoprefixer": "^10.2.5",
"html-webpack-plugin": "^4.5.0", "html-webpack-plugin": "^4.5.0",
"opentype.js": "^1.3.3", "postcss": "^8.2.13",
"postcss-loader": "4.0.2", "postcss-loader": "4.0.2",
"tailwindcss": "^2.1.2", "tailwindcss": "^2.1.2"
"worker-loader": "^3.0.7"
} }
} }

View File

@@ -0,0 +1,623 @@
(function(f) {
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f()
} else if (typeof define === "function" && define.amd) {
define([], f)
} else {
var g;
if (typeof window !== "undefined") {
g = window
} else if (typeof global !== "undefined") {
g = global
} else if (typeof self !== "undefined") {
g = self
} else {
g = this
}
g.jscadWorker = f()
}
})(function() {
// multi purpose module
const setPoints = (points, p, i)=>{
points[i++] = p[0]
points[i++] = p[1]
points[i++] = p[2] || 0
}
function CSG2Vertices(csg){
let idx = 0
let vLen = 0, iLen = 0
for (let poly of csg.polygons){
let len = poly.vertices.length
vLen += len *3
iLen += 3 * (len-2)
}
const vertices = new Float32Array(vLen)
const indices = vLen > 65535 ? new Uint32Array(iLen) : new Uint16Array(iLen)
let vertOffset = 0
let indOffset = 0
let posOffset = 0
let first = 0
for (let poly of csg.polygons){
let arr = poly.vertices
let len = arr.length
first = posOffset
vertices.set(arr[0], vertOffset)
vertOffset +=3
vertices.set(arr[1], vertOffset)
vertOffset +=3
posOffset +=2
for(let i=2; i<len; i++){
vertices.set(arr[i], vertOffset)
indices[indOffset++] = first
indices[indOffset++] = first + i -1
indices[indOffset++] = first + i
vertOffset += 3
posOffset += 1
}
}
return {vertices, indices, type:'mesh'}
}
function CSG2LineVertices(csg){
let vLen = csg.points.length * 3
if(csg.isClosed) vLen += 3
var vertices = new Float32Array(vLen)
csg.points.forEach((p,idx)=>setPoints(vertices, p, idx * 3 ))
if(csg.isClosed){
setPoints(vertices, csg.points[0], vertices.length - 3 )
}
return {vertices, type:'line'}
}
function CSG2LineSegmentsVertices(csg){
let vLen = csg.sides.length * 6
var vertices = new Float32Array(vLen)
csg.sides.forEach((side,idx)=>{
let i = idx * 6
setPoints(vertices, side[0], i)
setPoints(vertices, side[1], i+3)
})
return {vertices, type:'lines'}
}
function CSGCached(func, data, cacheKey, transferable){
cacheKey = cacheKey || data
let geo = CSGToBuffers.cache.get(cacheKey)
if(geo) return geo
geo = func(data)
// fill transferable array for postMessage optimization
if(transferable){
const {vertices, indices} = geo
transferable.push(vertices)
if(indices) transferable.push(indices)
}
CSGToBuffers.cache.set(cacheKey, geo)
return geo
}
function CSGToBuffers(csg, transferable){
let obj
if(csg.polygons) obj = CSGCached(CSG2Vertices,csg,csg.polygons, transferable)
if(csg.sides && !csg.points) obj = CSGCached(CSG2LineSegmentsVertices,csg,csg.sides, transferable)
if(csg.points) obj = CSGCached(CSG2LineVertices,csg,csg.points, transferable)
return obj
}
CSGToBuffers.clearCache = ()=>{CSGToBuffers.cache = new WeakMap()}
CSGToBuffers.clearCache()
let workerBaseURI
function require(url){
url = require.alias[url] || url
if(url[0] != '/' && url.substr(0,2) != './' && url.substr(0,4) != 'http') url = 'https://unpkg.com/'+url
let exports=require.cache[url]; //get from cache
if (!exports) { //not cached
let module = requireModule(url)
require.cache[url] = exports = module.exports; //cache obj exported by module
}
return exports; //require returns object exported by module
}
function requireFile(url){
try{
let X=new XMLHttpRequest();
X.open("GET", new URL(url,workerBaseURI), 0); // sync
X.send();
if (X.status && X.status !== 200) throw new Error(X.statusText);
return X.responseText;
}catch(e){
console.log('problem loading url ',url,'base',workerBaseURI,' error:',e.message)
throw e
}
}
function requireModule(url, source){
try {
const exports={};
if(!source) source = requireFile(url)
const module = { id: url, uri: url, exports:exports, source }; //according to node.js modules
// fix, add comment to show source on Chrome Dev Tools
source="//@ sourceURL="+url+"\n" + source;
//------
const anonFn = new Function("require", "exports", "module", source); //create a Fn with module code, and 3 params: require, exports & module
anonFn(require, exports, module); // call the Fn, Execute the module
return module
} catch (err) {
console.error("Error loading module "+url, err.message);
throw err;
}
}
require.cache = {}
require.alias = {}
const initCanvas = (canvas, callback)=>{
// convert HTML events (mouse movement) to viewer changes
let lastX = 0
let lastY = 0
let pointerDown = false
const moveHandler = (ev) => {
if(!pointerDown) return
const cmd = {
worker: 'render',
dx: lastX - ev.pageX,
dy: ev.pageY - lastY
}
const shiftKey = (ev.shiftKey === true) || (ev.touches && ev.touches.length > 2)
cmd.action = shiftKey ? 'pan':'rotate'
callback(cmd)
lastX = ev.pageX
lastY = ev.pageY
ev.preventDefault()
}
const downHandler = (ev) => {
pointerDown = true
lastX = ev.pageX
lastY = ev.pageY
canvas.setPointerCapture(ev.pointerId)
ev.preventDefault()
}
const upHandler = (ev) => {
pointerDown = false
canvas.releasePointerCapture(ev.pointerId)
ev.preventDefault()
}
const wheelHandler = (ev) => {
callback({action:'zoom', dy:ev.deltaY, worker: 'render'})
ev.preventDefault()
}
canvas.onpointermove = moveHandler
canvas.onpointerdown = downHandler
canvas.onpointerup = upHandler
canvas.onwheel = wheelHandler
}
const cmdHandler = (handlers)=>(cmd)=>{
const fn = handlers[cmd.action]
if (!fn) throw new Error('no handler for type: ' + cmd.action)
fn(cmd);
}
const makeScriptWorker = ({callback, convertToSolids})=>{
let workerBaseURI, onInit
function runMain(params={}){
let time = Date.now()
let solids
let transfer = []
try{
solids = main(params)
}catch(e){
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()}, transfer)
return
}
let solidsTime = Date.now() - time
scriptStats = `generate solids ${solidsTime}ms`
if(convertToSolids === 'buffers'){
CSGToBuffers.clearCache()
entities = solids.map((csg)=>{
let obj = CSGToBuffers(csg, transfer)
obj.color = csg.color
obj.transforms = csg.transforms
return obj
})
}else if(convertToSolids === 'regl'){
const { entitiesFromSolids } = require('@jscad/regl-renderer')
time = Date.now()
entities = entitiesFromSolids({}, solids)
scriptStats += ` convert to entities ${Date.now()-time}ms`
}else{
entities = solids
}
callback({action:'entities', worker:'render', entities, scriptStats}, transfer)
}
let initialized = false
const handlers = {
runScript: ({script,url, params={}})=>{
if(!initialized){
onInit = ()=>handlers.runScript({script,url, params})
}
let script_module
try{
script_module = requireModule(url,script)
}catch(e){
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()})
return
}
main = script_module.exports.main
let gp = script_module.exports.getParameterDefinitions
if(gp){
callback({action:'parameterDefinitions', worker:'main', data:gp()})
}
runMain(params)
},
updateParams: ({params={}})=>{
runMain(params)
},
init: (params)=>{
let {baseURI, alias=[]} = params
if(!baseURI && typeof document != 'undefined' && document.baseURI){
baseURI = document.baseURI
}
if(baseURI) workerBaseURI = baseURI.toString()
alias.forEach(arr=>{
let [orig, ...aliases] = arr
aliases.forEach(a=>{
require.alias[a] = orig
if(a.toLowerCase().substr(-3)!=='.js') require.alias[a+'.js'] = orig
})
})
initialized = true
if(onInit) onInit()
},
}
return {
// called from outside to pass mesasges into worker
postMessage: cmdHandler(handlers),
}
}
/** Make render worker */
const makeRenderWorker = ()=>{
let perspectiveCamera
const state = {}
const rotateSpeed = 0.002
const panSpeed = 1
const zoomSpeed = 0.08
let rotateDelta = [0, 0]
let panDelta = [0, 0]
let zoomDelta = 0
let updateRender = true
let orbitControls, renderOptions, gridOptions, axisOptions, renderer
let entities = []
function createContext (canvas, contextAttributes) {
function get (type) {
try {
return {gl:canvas.getContext(type, contextAttributes), type}
} catch (e) {
return null
}
}
return (
get('webgl2') ||
get('webgl') ||
get('experimental-webgl') ||
get('webgl-experimental')
)
}
const startRenderer = ({canvas, cameraPosition, cameraTarget, axis={}, grid={}})=>{
const { prepareRender, drawCommands, cameras, controls } = require('@jscad/regl-renderer')
perspectiveCamera = cameras.perspective
orbitControls = controls.orbit
state.canvas = canvas
state.camera = Object.assign({}, perspectiveCamera.defaults)
if(cameraPosition) state.camera.position = cameraPosition
if(cameraTarget) state.camera.target = cameraTarget
resize({ width:canvas.width, height:canvas.height })
state.controls = orbitControls.defaults
const {gl, type} = createContext(canvas)
// prepare the renderer
const setupOptions = {
glOptions: {gl}
}
if(type == 'webgl'){
setupOptions.glOptions.optionalExtensions = ['oes_element_index_uint']
}
renderer = prepareRender(setupOptions)
gridOptions = {
visuals: {
drawCmd: 'drawGrid',
show: grid.show || grid.show === undefined ,
color: grid.color || [0, 0, 0, 1],
subColor: grid.subColor || [0, 0, 1, 0.5],
fadeOut: false,
transparent: true
},
size: grid.size || [200, 200],
ticks: grid.ticks || [10, 1]
}
axisOptions = {
visuals: {
drawCmd: 'drawAxis',
show: axis.show || axis.show === undefined
},
size: axis.size || 100,
}
// assemble the options for rendering
renderOptions = {
camera: state.camera,
drawCommands: {
drawAxis: drawCommands.drawAxis,
drawGrid: drawCommands.drawGrid,
drawLines: drawCommands.drawLines,
drawMesh: drawCommands.drawMesh
},
// define the visual content
entities: [
gridOptions,
axisOptions,
...entities
]
}
// the heart of rendering, as themes, controls, etc change
updateView()
}
let renderTimer
const tmFunc = typeof requestAnimationFrame === 'undefined' ? setTimeout : requestAnimationFrame
function updateView(delay=8){
if(renderTimer || !renderer) return
renderTimer = tmFunc(updateAndRender,delay)
}
const doRotatePanZoom = () => {
if (rotateDelta[0] || rotateDelta[1]) {
const updated = orbitControls.rotate({ controls: state.controls, camera: state.camera, speed: rotateSpeed }, rotateDelta)
state.controls = { ...state.controls, ...updated.controls }
rotateDelta = [0, 0]
}
if (panDelta[0] || panDelta[1]) {
const updated = orbitControls.pan({ controls:state.controls, camera:state.camera, speed: panSpeed }, panDelta)
state.controls = { ...state.controls, ...updated.controls }
panDelta = [0, 0]
state.camera.position = updated.camera.position
state.camera.target = updated.camera.target
}
if (zoomDelta) {
const updated = orbitControls.zoom({ controls:state.controls, camera:state.camera, speed: zoomSpeed }, zoomDelta)
state.controls = { ...state.controls, ...updated.controls }
zoomDelta = 0
}
}
const updateAndRender = (timestamp) => {
renderTimer = null
doRotatePanZoom()
const updates = orbitControls.update({ controls: state.controls, camera: state.camera })
state.controls = { ...state.controls, ...updates.controls }
if(state.controls.changed) updateView(16) // for elasticity in rotate / zoom
state.camera.position = updates.camera.position
perspectiveCamera.update(state.camera)
renderOptions.entities = [
gridOptions,
axisOptions,
...entities
]
let time = Date.now()
renderer(renderOptions)
if(updateRender){
updateRender = '';
}
}
function resize({width,height}){
state.canvas.width = width
state.canvas.height = height
perspectiveCamera.setProjection(state.camera, state.camera, { width, height })
perspectiveCamera.update(state.camera, state.camera)
updateView()
}
const handlers = {
pan: ({dx,dy})=>{
panDelta[0] += dx
panDelta[1] += dy
updateView()
},
rotate: ({dx,dy})=>{
rotateDelta[0] -= dx
rotateDelta[1] -= dy
updateView()
},
zoom: ({dy})=>{
zoomDelta += dy
updateView()
},
resize,
entities: (params)=>{
entities = params.entities
updateRender = params.scriptStats
updateView()
},
init: (params)=>{
if(params.canvas) startRenderer(params)
initialized = true
},
}
return {
// called from outside to pass mesasges into worker
postMessage: cmdHandler(handlers),
}
}
return (params)=>{
let { canvas, baseURI=(typeof document === 'undefined') ? '':document.location.toString(), scope='main', renderInWorker, render, callback=()=>{}, scriptUrl='demo-worker.js', alias, convertToSolids=false } = params
// by default 'render' messages go outside of this instance (result of modeling)
let sendToRender = callback
let scriptWorker, renderWorker
workerBaseURI = baseURI
const sendCmd = (params, transfer)=>{
if(params.worker === 'render')
sendToRender(params, transfer)
else if(params.worker === 'script')
scriptWorker.postMessage(params, transfer)
else{
// parameter definitions will arrive from scriptWorker
callback(params, transfer)
}
}
const updateSize = function({width,height}){
sendCmd({ action:'resize', worker:'render', width: canvas.offsetWidth, height: canvas.offsetHeight})
}
renderInWorker = !!(canvas && renderInWorker && canvas.transferControlToOffscreen)
const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render)
// worker is in current thread
if(makeRenderWorkerHere){
renderWorker = makeRenderWorker({callback:sendCmd})
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
}
if(scope === 'main'){
// let extraScript = renderInWorker ? `,'https://unpkg.com/@jscad/regl-renderer'`:''
let script =`let baseURI = '${baseURI}'
importScripts(new URL('${scriptUrl}',baseURI))
let worker = jscadWorker({
baseURI: baseURI,
convertToSolids: ${convertToSolids},
scope:'worker',
callback:(params)=>self.postMessage(params),
render:${renderInWorker}
})
self.addEventListener('message', (e)=>worker.postMessage(e.data))
`
let blob = new Blob([script],{type: 'text/javascript'})
scriptWorker = new Worker(window.URL.createObjectURL(blob))
scriptWorker.addEventListener('message',(e)=>sendCmd(e.data))
scriptWorker.postMessage({action:'init', baseURI, alias})
if(renderInWorker) renderWorker = scriptWorker
if(canvas){
initCanvas(canvas, sendCmd)
window.addEventListener('resize',updateSize)
}
}else{
scriptWorker = makeScriptWorker({callback:sendCmd, convertToSolids})
callback({action:'workerInit',worker:'main'})
}
if(canvas){
// redirect 'render' messages to renderWorker
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
let width = canvas.width = canvas.clientWidth
let height = canvas.height = canvas.clientHeight
if(scope == 'main'){
const offscreen = renderInWorker ? canvas.transferControlToOffscreen() : canvas
renderWorker.postMessage({action:'init', worker:'render', canvas:offscreen, width, height}, [offscreen])
}
}
return {
updateSize,
updateParams:({params={}})=>sendCmd({ action:'updateParams', worker:'script', params}),
runScript: ({script,url=''})=>sendCmd({ action:'runScript', worker:'script', script, url}),
postMessage: sendCmd,
}
}
// multi purpose module
});

View File

@@ -44,24 +44,28 @@ const Routes = () => {
<Route notfound page={NotFoundPage} /> <Route notfound page={NotFoundPage} />
{/* Ownership enforced routes */} {/* Ownership enforced routes */}
<Route path="/u/{userName}/new" page={NewPartPage} name="newPart" /> <Route path="/u/{userName}/new" page={NewProjectPage} name="newProject" />
<Private unauthenticated="home" role="user"> <Private unauthenticated="home" role="user">
<Route path="/u/{userName}/edit" page={EditUserPage} name="editUser" /> <Route path="/u/{userName}/edit" page={EditUserPage} name="editUser" />
<Route path="/u/{userName}/{partTitle}/edit" page={EditPartPage} name="editPart" /> <Route path="/u/{userName}/{projectTitle}/edit" page={EditProjectPage} name="editProject" />
</Private> </Private>
{/* End ownership enforced routes */} {/* End ownership enforced routes */}
<Route path="/draft" page={DraftPartPage} name="draftPart" /> <Route path="/draft/{cadPackage}" page={DraftProjectPage} name="draftProject" />
<Route path="/u/{userName}" page={UserPage} name="user" /> <Route path="/u/{userName}" page={UserPage} name="user" />
<Route path="/u/{userName}/{partTitle}" page={PartPage} name="part" /> <Route path="/u/{userName}/{projectTitle}" page={ProjectPage} name="project" />
<Route path="/u/{userName}/{partTitle}/ide" page={IdePartPage} name="ide" /> <Route path="/u/{userName}/{projectTitle}/ide" page={IdeProjectPage} name="ide" />
<Route path="/u/{userName}/{projectTitle}/social-card" page={SocialCardPage} name="socialCard" />
<Private unauthenticated="home" role="admin"> <Private unauthenticated="home" role="admin">
<Route path="/admin/users" page={UsersPage} name="users" /> <Route path="/admin/users" page={UsersPage} name="users" />
<Route path="/admin/parts" page={AdminPartsPage} name="parts" /> <Route path="/admin/projects" page={AdminProjectsPage} name="projects" />
<Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" /> <Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" />
<Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" /> <Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" />
<Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" /> <Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" />
{/* Retired for now but might want to bring it back, delete if older that I dunno late 2021 */}
{/* <Route path="/admin/email" page={AdminEmailPage} name="adminEmail" /> */}
</Private> </Private>
</Router> </Router>
) )

View File

@@ -1,11 +1,12 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import { QUERY } from 'src/components/AdminPartsCell' import { QUERY } from 'src/components/AdminProjectsCell/AdminProjectsCell'
const DELETE_PART_MUTATION = gql` const DELETE_PROJECT_MUTATION_ADMIN = gql`
mutation DeletePartMutation($id: String!) { mutation DeleteProjectMutationAdmin($id: String!) {
deletePart(id: $id) { deleteProject(id: $id) {
id id
} }
} }
@@ -21,10 +22,6 @@ const truncate = (text) => {
return output return output
} }
const jsonTruncate = (obj) => {
return truncate(JSON.stringify(obj, null, 2))
}
const timeTag = (datetime) => { const timeTag = (datetime) => {
return ( return (
<time dateTime={datetime} title={datetime}> <time dateTime={datetime} title={datetime}>
@@ -37,11 +34,10 @@ const checkboxInputTag = (checked) => {
return <input type="checkbox" checked={checked} disabled /> return <input type="checkbox" checked={checked} disabled />
} }
const AdminParts = ({ parts }) => { const AdminProjects = ({ projects }) => {
const { addMessage } = useFlash() const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION_ADMIN, {
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
onCompleted: () => { onCompleted: () => {
addMessage('Part deleted.', { classes: 'rw-flash-success' }) toast.success('Project deleted.')
}, },
// This refetches the query on the list page. Read more about other ways to // This refetches the query on the list page. Read more about other ways to
// update the cache over here: // update the cache over here:
@@ -51,8 +47,8 @@ const AdminParts = ({ parts }) => {
}) })
const onDeleteClick = (id) => { const onDeleteClick = (id) => {
if (confirm('Are you sure you want to delete part ' + id + '?')) { if (confirm('Are you sure you want to delete project ' + id + '?')) {
deletePart({ variables: { id } }) deleteProject({ variables: { id } })
} }
} }
@@ -74,44 +70,44 @@ const AdminParts = ({ parts }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{parts.map((part) => ( {projects.map((project) => (
<tr key={part.id}> <tr key={project.id}>
<td>{truncate(part.id)}</td> <td>{truncate(project.id)}</td>
<td>{truncate(part.title)}</td> <td>{truncate(project.title)}</td>
<td>{truncate(part.description)}</td> <td>{truncate(project.description)}</td>
<td>{truncate(part.code)}</td> <td>{truncate(project.code)}</td>
<td>{truncate(part.mainImage)}</td> <td>{truncate(project.mainImage)}</td>
<td>{timeTag(part.createdAt)}</td> <td>{timeTag(project.createdAt)}</td>
<td>{timeTag(part.updatedAt)}</td> <td>{timeTag(project.updatedAt)}</td>
<td>{truncate(part.userId)}</td> <td>{truncate(project.userId)}</td>
<td>{checkboxInputTag(part.deleted)}</td> <td>{checkboxInputTag(project.deleted)}</td>
<td> <td>
<nav className="rw-table-actions"> <nav className="rw-table-actions">
<Link <Link
to={routes.part({ to={routes.project({
userName: part?.user?.userName, userName: project?.user?.userName,
partTitle: part?.title, projectTitle: project?.title,
})} })}
title={'Show part ' + part.id + ' detail'} title={'Show project ' + project.id + ' detail'}
className="rw-button rw-button-small" className="rw-button rw-button-small"
> >
Show Show
</Link> </Link>
<Link <Link
to={routes.editPart({ to={routes.editProject({
userName: part?.user?.userName, userName: project?.user?.userName,
partTitle: part?.title, projectTitle: project?.title,
})} })}
title={'Edit part ' + part.id} title={'Edit project ' + project.id}
className="rw-button rw-button-small rw-button-blue" className="rw-button rw-button-small rw-button-blue"
> >
Edit Edit
</Link> </Link>
<a <a
href="#" href="#"
title={'Delete part ' + part.id} title={'Delete project ' + project.id}
className="rw-button rw-button-small rw-button-red" className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(part.id)} onClick={() => onDeleteClick(project.id)}
> >
Delete Delete
</a> </a>
@@ -125,4 +121,4 @@ const AdminParts = ({ parts }) => {
) )
} }
export default AdminParts export default AdminProjects

View File

@@ -1,10 +1,10 @@
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import AdminParts from 'src/components/AdminParts' import AdminProjects from 'src/components/AdminProjects/AdminProjects'
export const QUERY = gql` export const QUERY = gql`
query PARTS { query PROJECTS_ADMIN {
parts { projects {
id id
title title
description description
@@ -26,14 +26,14 @@ export const Loading = () => <div>Loading...</div>
export const Empty = () => { export const Empty = () => {
return ( return (
<div className="rw-text-center"> <div className="rw-text-center">
{'No parts yet. '} {'No projects yet. '}
<Link to={routes.newPart()} className="rw-link"> <Link to={routes.newProject()} className="rw-link">
{'Create one?'} {'Create one?'}
</Link> </Link>
</div> </div>
) )
} }
export const Success = ({ parts }) => { export const Success = ({ projects }) => {
return <AdminParts parts={parts} /> return <AdminProjects projects={projects} />
} }

View File

@@ -1,42 +0,0 @@
import { getActiveClasses } from 'get-active-classes'
import { Link, routes } from '@redwoodjs/router'
import InputText from 'src/components/InputText'
const Breadcrumb = ({
userName,
partTitle,
onPartTitleChange,
className,
isInvalid,
}) => {
return (
<h3 className={getActiveClasses('text-2xl font-roboto', className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">
.
</div>
<span
className={getActiveClasses({
'text-gray-500': !onPartTitleChange,
'text-gray-400': onPartTitleChange,
})}
>
<Link to={routes.user({ userName })}>{userName}</Link>
</span>
<div className="w-1 inline-block bg-gray-400 text-gray-400 mx-3 transform -skew-x-20">
.
</div>
<InputText
value={partTitle}
onChange={onPartTitleChange}
isEditable={onPartTitleChange}
className={getActiveClasses('text-indigo-800 text-2xl', {
'-ml-2': !onPartTitleChange,
})}
isInvalid={isInvalid}
/>
</h3>
)
}
export default Breadcrumb

View File

@@ -1,7 +0,0 @@
import Breadcrumb from './Breadcrumb'
export const generated = () => {
return <Breadcrumb />
}
export default { title: 'Components/Breadcrumb' }

View File

@@ -19,7 +19,7 @@ const Button = ({
'text-red-600 bg-red-200 border border-red-600': type === 'danger', 'text-red-600 bg-red-200 border border-red-600': type === 'danger',
'text-indigo-600': !type, 'text-indigo-600': !type,
}, },
'flex items-center bg-opacity-50 rounded-xl p-2 px-6', 'flex items-center bg-opacity-50 rounded p-2 px-6',
{ {
'mx-px transform hover:-translate-y-px transition-all duration-150': 'mx-px transform hover:-translate-y-px transition-all duration-150':
shouldAnimateHover && !disabled, shouldAnimateHover && !disabled,
@@ -29,7 +29,7 @@ const Button = ({
onClick={onClick} onClick={onClick}
> >
{children} {children}
<Svg className="w-6 ml-4" name={iconName} /> {iconName && <Svg className="w-6 ml-4" name={iconName} />}
</button> </button>
) )
} }

View File

@@ -0,0 +1,30 @@
import { ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
interface CadPackageProps {
cadPackage: string
className?: string
}
const CadPackage = ({ cadPackage, className = '' }: CadPackageProps) => {
const cadName = ideTypeNameMap[cadPackage] || ''
const isOpenScad = cadPackage === 'openscad'
const isCadQuery = cadPackage === 'cadquery'
return (
<div
className={
`grid grid-flow-col-dense items-center gap-2 cursor-default text-gray-100 ${
isOpenScad && 'bg-yellow-800'
} ${isCadQuery && 'bg-ch-blue-300'} bg-opacity-30 ` + className
}
>
<div
className={`${isOpenScad && 'bg-yellow-200'} ${
isCadQuery && 'bg-blue-800'
} w-5 h-5 rounded-full`}
/>
<div>{cadName}</div>
</div>
)
}
export default CadPackage

View File

@@ -0,0 +1,193 @@
import { useState } from 'react'
import { toast } from '@redwoodjs/web/toast'
import Popover from '@material-ui/core/Popover'
import Svg from 'src/components/Svg/Svg'
import Button from 'src/components/Button/Button'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
import { toJpeg } from 'html-to-image'
const anchorOrigin = {
vertical: 'bottom',
horizontal: 'center',
}
const transformOrigin = {
vertical: 'top',
horizontal: 'center',
}
const CaptureButton = ({
canEdit,
TheButton,
shouldUpdateImage,
projectTitle,
userName,
}) => {
const [captureState, setCaptureState] = useState<any>({})
const [anchorEl, setAnchorEl] = useState(null)
const [whichPopup, setWhichPopup] = useState(null)
const { state, project } = useIdeContext()
const ref = React.useRef<HTMLDivElement>(null)
const { updateProjectImages } = useUpdateProjectImages({})
const onCapture = async () => {
const threeInstance = state.threeInstance
const isOpenScadImage = state?.objectData?.type === 'png'
let imgBlob
let image64
if (!isOpenScadImage) {
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
image64 = blobTo64(
await canvasToBlob(threeInstance, { width: 500, height: 522 })
)
} else {
imgBlob = state.objectData.data
image64 = blobTo64(state.objectData.data)
}
const config = {
image: await imgBlob,
currImage: project?.mainImage,
imageObjectURL: window.URL.createObjectURL(await imgBlob),
callback: uploadAndUpdateImage,
cloudinaryImgURL: '',
updated: false,
image64: await image64,
}
setCaptureState(config)
async function uploadAndUpdateImage() {
const upload = async () => {
const socialCard64 = toJpeg(ref.current, {
cacheBust: true,
quality: 0.7,
})
// uploading in two separate mutations because of the 100kb limit of the lambda functions
const imageUploadPromise1 = updateProjectImages({
variables: {
id: project?.id,
mainImage64: await config.image64,
},
})
const imageUploadPromise2 = updateProjectImages({
variables: {
id: project?.id,
socialCard64: await socialCard64,
},
})
return Promise.all([imageUploadPromise2, imageUploadPromise1])
}
const promise = upload()
toast.promise(promise, {
loading: 'Saving Image/s',
success: <b>Image/s saved!</b>,
error: <b>Problem saving.</b>,
})
const [{ data }] = await promise
return data?.updateProjectImages?.mainImage
}
// if there isn't a screenshot saved yet, just go ahead and save right away
if (shouldUpdateImage) {
config.cloudinaryImgURL = await uploadAndUpdateImage()
config.updated = true
setCaptureState(config)
}
}
const handleClick = ({ event, whichPopup }) => {
setAnchorEl(event.currentTarget)
setWhichPopup(whichPopup)
}
const handleClose = () => {
setAnchorEl(null)
setWhichPopup(null)
}
return (
<div>
{canEdit && (
<div>
<TheButton
onClick={async (event) => {
handleClick({ event, whichPopup: 'capture' })
onCapture()
}}
/>
<Popover
id={'capture-popover'}
open={whichPopup === 'capture'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-4 text-gray-500">
{!captureState ? (
'Loading...'
) : (
<div className="">
<div className="text-lg">Thumbnail</div>
<div
className="rounded"
style={{ width: 'fit-content', overflow: 'hidden' }}
>
<img src={captureState.imageObjectURL} className="w-32" />
</div>
</div>
)}
<div className="text-lg mt-4">Social Media Card</div>
<div className="rounded-lg shadow-md overflow-hidden">
<div
className="transform scale-50 origin-top-left"
style={{ width: '600px', height: '315px' }}
>
<div style={{ width: '1200px', height: '630px' }} ref={ref}>
<SocialCardCell
userName={userName}
projectTitle={projectTitle}
image64={captureState.image64}
/>
</div>
</div>
</div>
<div className="mt-4 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<Button
iconName="refresh"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-200 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover
onClick={async () => {
const cloudinaryImg = await captureState.callback()
setCaptureState({
...captureState,
currImage: cloudinaryImg,
updated: true,
})
}}
>
Update Project Images
</Button>
) : (
<div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Project Images Updated
</div>
)}
</div>
</div>
</Popover>
</div>
)}
</div>
)
}
export default CaptureButton

View File

@@ -0,0 +1,271 @@
import { useRender } from 'src/components/IdeWrapper/useRender'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { Switch } from '@headlessui/react'
import Svg from 'src/components/Svg/Svg'
import {
CadhubStringParam,
CadhubBooleanParam,
CadhubNumberParam,
} from './customizerConverter'
import { debounce } from 'lodash'
const Customizer = () => {
const [open, setOpen] = React.useState(false)
const [shouldLiveUpdate, setShouldLiveUpdate] = React.useState(false)
const { state, thunkDispatch } = useIdeContext()
const customizerParams = state?.customizerParams
const currentParameters = state?.currentParameters || {}
const handleRender = useRender()
const updateCustomizerParam = (paramName: string, paramValue: any) => {
const payload = {
...currentParameters,
[paramName]: paramValue,
}
thunkDispatch({ type: 'setCurrentCustomizerParams', payload })
shouldLiveUpdate && setTimeout(() => handleRender())
}
if (!customizerParams?.length) return null
return (
<div
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
open ? 'h-2/3' : ''
}`}
>
<div className="flex justify-between px-6 py-2 items-center">
<div className="grid grid-flow-col-dense gap-6 items-center">
<button className="px-2" onClick={() => setOpen(!open)}>
<Svg
name="chevron-down"
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
/>
</button>
<div>Parameters</div>
</div>
{open && (
<>
<div className="flex items-center">
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
<Switch
checked={shouldLiveUpdate}
onChange={(newValue) => {
if (newValue) handleRender()
setShouldLiveUpdate(newValue)
}}
className={`${
shouldLiveUpdate ? 'bg-ch-purple-600' : 'bg-ch-gray-300'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6`}
>
<span
className={`${
shouldLiveUpdate ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
<button
className={`px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-800 ${
shouldLiveUpdate && 'bg-opacity-30 cursor-default'
}`}
onClick={handleRender}
disabled={shouldLiveUpdate}
>
Update
</button>
</div>
</>
)}
</div>
<div className={`${open ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}>
<div>
{customizerParams.map((param, index) => {
const otherProps = {
value: currentParameters[param.name],
onChange: (value) => updateCustomizerParam(param.name, value),
}
if (param.type === 'string') {
return <StringParam key={index} param={param} {...otherProps} />
} else if (param.type === 'number') {
return <NumberParam key={index} param={param} {...otherProps} />
} else if (param.type === 'boolean') {
return <BooleanParam key={index} param={param} {...otherProps} />
}
return <div key={index}>{JSON.stringify(param)}</div>
})}
</div>
</div>
</div>
)
}
export default Customizer
function CustomizerParamBase({
name,
caption,
children,
}: {
name: string
caption: string
children: React.ReactNode
}) {
return (
<li
className="grid items-center my-2"
style={{ gridTemplateColumns: 'auto 8rem' }}
>
<div className=" text-sm font-fira-sans">
<div className="font-bold text-base">{name}</div>
<div>{caption}</div>
</div>
<div className="w-full">{children}</div>
</li>
)
}
function BooleanParam({
param,
value,
onChange,
}: {
param: CadhubBooleanParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<Switch
checked={value}
onChange={(newValue) => {
onChange(newValue)
}}
className={`${
value ? 'bg-ch-gray-300' : 'bg-ch-gray-600'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6 border border-ch-gray-300`}
>
<span
className={`${
value ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
</CustomizerParamBase>
)
}
function StringParam({
param,
value,
onChange,
}: {
param: CadhubStringParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<input
className="bg-transparent h-8 border border-ch-gray-300 px-2 text-sm w-full"
type="text"
value={value}
placeholder={param.placeholder}
onChange={({ target }) => onChange(target?.value)}
/>
</CustomizerParamBase>
)
}
function NumberParam({
param,
value,
onChange,
}: {
param: CadhubNumberParam
value: any
onChange: Function
}) {
const [isFocused, isFocusedSetter] = React.useState(false)
const [localValue, localValueSetter] = React.useState(0)
const [isLocked, isLockedSetter] = React.useState(false)
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
const handleRender = useRender()
const liveRenderHandler = debounce((a) => handleRender(a), 250)
const step = param.step || 1
const live = false // TODO get from param
let decimal = 0
if('decimal' in param){
decimal = param.decimal
}else{
let str = String(step)
const idx = str.indexOf('.')
if(idx !== -1) decimal = str.length - idx - 1
}
const commitChange = () => {
let num = localValue
if (typeof param.step === 'number') {
num = Math.round(num / step) * step
}
if (typeof param.min === 'number') {
num = Math.max(param.min, num)
}
if (typeof param.max === 'number') {
num = Math.min(param.max, num)
}
num = Number(num.toFixed(decimal))
localValueSetter(num)
onChange(num)
}
React.useEffect(() => {
if (!isFocused) commitChange()
}, [isFocused])
React.useEffect(() => localValueSetter(value), [value])
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<div className="flex h-8 border border-ch-gray-300">
<input
className={`bg-transparent px-2 text-sm w-full ${
(param.max && param.max < localValue) ||
(param.min && param.min > localValue)
? 'text-red-500'
: ''
}`}
type="number"
value={localValue}
onFocus={() => isFocusedSetter(true)}
onBlur={() => isFocusedSetter(false)}
onKeyDown={({ key }) => key === 'Enter' && commitChange()}
onChange={({ target }) => {
const num = Number(target?.value)
localValueSetter(num)
}}
max={param.max}
min={param.min}
step={step}
/>
<div
className="w-6 border-l border-ch-gray-500 items-center hidden md:flex"
style={{ cursor: 'ew-resize' }}
onMouseDown={({ target }) => {
isLockedSetter(true)
target?.requestPointerLock?.()
pixelsDraggedSetter(localValue)
}}
onMouseUp={() => {
isLockedSetter(false)
document?.exitPointerLock?.()
commitChange()
}}
onMouseMove={({ movementX }) => {
if (isLocked && movementX) {
pixelsDraggedSetter(pixelsDragged + (movementX * step) / 8) // one step per 8 pixels
const decimalFixed = Number((Math.round(pixelsDragged / step) * step).toFixed(decimal))
localValueSetter(decimalFixed)
if(live) liveRenderHandler({[param.name]: decimalFixed})
}
}}
>
<Svg className="w-6" name="switch-horizontal" />
</div>
</div>
</CustomizerParamBase>
)
}

View File

@@ -0,0 +1,120 @@
// CadHub
type CadhubTypeNames = 'number' | 'string' | 'boolean'
interface CadhubParamBase {
type: CadhubTypeNames
caption: string
name: string
}
export interface CadhubStringParam extends CadhubParamBase {
type: 'string'
initial: string
placeholder?: string
maxLength?: number
}
export interface CadhubBooleanParam extends CadhubParamBase {
type: 'boolean'
initial?: boolean
}
export interface CadhubNumberParam extends CadhubParamBase {
type: 'number'
initial: number
min?: number
max?: number
step?: number
decimal?: number
}
export type CadhubParams =
| CadhubStringParam
| CadhubBooleanParam
| CadhubNumberParam
// OpenSCAD
const openscadValues = `
// slider widget for number with max. value
sliderWithMax =34; // [50]
// slider widget for number in range
sliderWithRange =34; // [10:100]
//step slider for number
stepSlider=2; //[0:5:100]
// slider widget for number in range
sliderCentered =0; // [-10:0.1:10]
// spinbox with step size 1
Spinbox= 5;
// Text box for string
String="hello";
// Text box for string with length 8
String2="length"; //8
//description
Variable = true;
`
const openscadConverted: CadhubParams[] = [
{
type: 'number',
name: 'sliderWithMax',
caption: 'slider widget for number with max. value',
initial: 34,
step: 1,
max: 50,
},
{
type: 'number',
name: 'sliderWithRange',
caption: 'slider widget for number in range',
initial: 34,
step: 1,
min: 10,
max: 100,
},
{
type: 'number',
name: 'stepSlider',
caption: 'step slider for number',
initial: 2,
step: 5,
min: 0,
max: 100,
},
{
type: 'number',
name: 'sliderCentered',
caption: 'slider widget for number in range',
initial: 0,
step: 0.1,
min: -10,
max: 10,
},
{
type: 'number',
name: 'Spinbox',
caption: 'spinbox with step size 1',
initial: 5,
step: 1,
},
{
type: 'string',
name: 'String',
caption: 'Text box for string',
initial: 'hello',
},
{
type: 'string',
name: 'String2',
caption: 'Text box for string with length 8',
initial: 'length',
maxLength: 8,
},
{ type: 'boolean', name: 'Variable', caption: 'description', initial: true },
]

View File

@@ -0,0 +1,31 @@
let timeoutId = 0
const DelayedPingAnimation = ({
isLoading: isLoading,
}: {
isLoading: boolean
}) => {
const [showLoading, setShowLoading] = React.useState(false)
React.useEffect(() => {
if (!isLoading && showLoading) {
setShowLoading(isLoading)
clearTimeout(timeoutId)
} else if (isLoading && !showLoading) {
timeoutId = setTimeout(() => {
setShowLoading(isLoading)
}, 300) as unknown as number
} else if (!isLoading) {
setShowLoading(isLoading)
clearTimeout(timeoutId)
}
}, [isLoading])
if (showLoading && isLoading)
return (
<div className="inset-0 absolute flex items-center justify-center">
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
</div>
)
return null
}
export default DelayedPingAnimation

View File

@@ -1,4 +1,5 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { navigate, routes } from '@redwoodjs/router' import { navigate, routes } from '@redwoodjs/router'
import SubjectAccessRequestForm from 'src/components/SubjectAccessRequestForm' import SubjectAccessRequestForm from 'src/components/SubjectAccessRequestForm'
@@ -33,15 +34,12 @@ const UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
export const Loading = () => <div>Loading...</div> export const Loading = () => <div>Loading...</div>
export const Success = ({ subjectAccessRequest }) => { export const Success = ({ subjectAccessRequest }) => {
const { addMessage } = useFlash()
const [updateSubjectAccessRequest, { loading, error }] = useMutation( const [updateSubjectAccessRequest, { loading, error }] = useMutation(
UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION, UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION,
{ {
onCompleted: () => { onCompleted: () => {
navigate(routes.subjectAccessRequests()) navigate(routes.subjectAccessRequests())
addMessage('SubjectAccessRequest updated.', { toast.success('SubjectAccessRequest updated.')
classes: 'rw-flash-success',
})
}, },
} }
) )

View File

@@ -1,10 +1,11 @@
import { useMutation, useFlash } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { navigate, routes } from '@redwoodjs/router' import { navigate, routes } from '@redwoodjs/router'
import UserProfile from 'src/components/UserProfile' import UserProfile from 'src/components/UserProfile'
export const QUERY = gql` export const QUERY = gql`
query FIND_USER_BY_ID($userName: String!) { query FIND_USER_BY_USERNAME($userName: String!) {
user: userName(userName: $userName) { user: userName(userName: $userName) {
id id
userName userName
@@ -22,6 +23,9 @@ const UPDATE_USER_MUTATION = gql`
updateUserByUserName(userName: $userName, input: $input) { updateUserByUserName(userName: $userName, input: $input) {
id id
userName userName
bio
name
image
} }
} }
` `
@@ -33,11 +37,10 @@ export const Empty = () => <div className="h-full">Empty</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div> export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ user, refetch, variables: { isEditable } }) => { export const Success = ({ user, refetch, variables: { isEditable } }) => {
const { addMessage } = useFlash()
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, { const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
onCompleted: ({ updateUserByUserName }) => { onCompleted: ({ updateUserByUserName }) => {
navigate(routes.user({ userName: updateUserByUserName.userName })) navigate(routes.user({ userName: updateUserByUserName.userName }))
addMessage('User updated.', { classes: 'rw-flash-success' }) toast.success('User updated.')
}, },
}) })

View File

@@ -0,0 +1,109 @@
import { useState } from 'react'
import { toast } from '@redwoodjs/web/toast'
import { Link, navigate, routes } from '@redwoodjs/router'
import { useUpdateProject } from 'src/helpers/hooks/useUpdateProject'
import Svg from 'src/components/Svg/Svg'
interface EditableProjectTitleProps {
id: string
userName: string
projectTitle: string
canEdit: boolean
shouldRouteToIde: boolean
}
const EditableProjectTitle = ({
id,
userName,
projectTitle,
canEdit,
shouldRouteToIde,
}: EditableProjectTitleProps) => {
const [inEditMode, setInEditMode] = useState(false)
const [newTitle, setNewTitle] = useState(projectTitle)
const inputRef = React.useRef(null)
const { updateProject, loading, error } = useUpdateProject({
onCompleted: ({ updateProject }) => {
const routeVars = {
userName: updateProject.user.userName,
projectTitle: updateProject.title,
}
navigate(
shouldRouteToIde ? routes.ide(routeVars) : routes.project(routeVars)
)
toast.success('Project updated.')
},
})
const onTitleChange = ({ target }) => {
if (target.value.length > 25) {
toast.error('Titles must be 25 or less characters')
}
setNewTitle(target.value.replace(/([^a-zA-Z\d_:])/g, '-').slice(0, 25))
}
return (
<>
{!inEditMode && (
<>
<Link
to={routes.project({
userName,
projectTitle,
})}
className="pl-4"
>
/{projectTitle}
</Link>
{canEdit && (
<button
onClick={() => {
setInEditMode(true)
setTimeout(() => inputRef?.current?.focus())
}}
>
<Svg name="pencil-solid" className="h-4 w-4 ml-4 mb-2" />
</button>
)}
</>
)}
{inEditMode && (
<>
<span className="flex items-center ml-4 border border-ch-gray-300 rounded-sm">
<span className="ml-1">/</span>
<input
className="pl-1 w-64 bg-ch-gray-900"
value={newTitle}
onChange={onTitleChange}
ref={inputRef}
onBlur={() =>
setTimeout(() => {
setInEditMode(false)
setNewTitle(projectTitle)
}, 300)
}
/>
</span>
<div className="flex items-center h-full">
<button
className="ml-4 grid grid-flow-col-dense p-px px-2 gap-2 bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 rounded-sm border border-ch-purple-400"
id="rename-button"
onClick={() =>
updateProject({ variables: { id, input: { title: newTitle } } })
}
>
<Svg
name="check"
className="w-6 h-6 text-ch-purple-500"
strokeWidth={3}
/>
<span>Rename</span>
</button>
</div>
</>
)}
</>
)
}
export default EditableProjectTitle

View File

@@ -0,0 +1,133 @@
import { Menu } from '@headlessui/react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import Svg from 'src/components/Svg/Svg'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import CadPackage from 'src/components/CadPackage/CadPackage'
const EditorMenu = () => {
const handleRender = useRender()
const saveCode = useSaveCode()
const { state, thunkDispatch } = useIdeContext()
const onRender = () => {
handleRender()
saveCode({ code: state.code })
}
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
ideType: state.ideType,
geometry: state.objectData?.data,
quality: state.objectData?.quality,
fileName: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
return (
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
<div className="flex items-center h-9 w-full cursor-grab">
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-2 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
<div className="grid grid-flow-col-dense gap-6 px-5">
<FileDropdown
handleRender={onRender}
handleStlDownload={handleStlDownload}
/>
<button className="cursor-not-allowed" disabled>
Edit
</button>
<ViewDropdown
handleLayoutReset={() => thunkDispatch({ type: 'resetLayout' })}
/>
</div>
<button
className="text-ch-gray-300 h-full cursor-not-allowed"
aria-label="editor settings"
disabled
>
<Svg name="gear" className="w-6 p-px" />
</button>
</div>
<CadPackage cadPackage={state.ideType} className="px-3" />
</div>
)
}
export default EditorMenu
function FileDropdown({ handleRender, handleStlDownload }) {
return (
<Dropdown name="File">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleRender}
>
Save &amp; Render{' '}
<span className="text-gray-400 pl-4">
{/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? (
<>
<Svg
name="mac-cmd-key"
className="h-3 w-3 inline-block text-left"
/>
S
</>
) : (
'Ctrl S'
)}
</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1 text-left`}
onClick={handleStlDownload}
>
Download STL
</button>
)}
</Menu.Item>
</Dropdown>
)
}
function ViewDropdown({ handleLayoutReset }) {
return (
<Dropdown name="View">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleLayoutReset}
>
Reset layout
</button>
)}
</Menu.Item>
</Dropdown>
)
}
function Dropdown({
name,
children,
}: {
name: string
children: React.ReactNode
}) {
return (
<div className="relative">
<Menu>
<Menu.Button className="text-gray-100">{name}</Menu.Button>
<Menu.Items className="absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700">
{children}
</Menu.Items>
</Menu>
</div>
)
}

View File

@@ -0,0 +1,84 @@
import { flow, identity } from 'lodash/fp'
import { fileSave } from 'browser-fs-access'
import { MeshBasicMaterial, Mesh, Scene } from 'three'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { requestRender, State } from 'src/helpers/hooks/useIdeState'
export const PullTitleFromFirstLine = (code = '') => {
const firstLine = code.split('\n').filter(identity)[0] || ''
if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
return 'object.stl'
}
return (
(firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
titleWithSpaces.replaceAll(/\s/g, '-')
) || 'object') + '.stl'
)
}
interface makeStlDownloadHandlerArgs {
geometry: any
fileName: string
type: State['objectData']['type']
ideType: State['ideType']
thunkDispatch: (a: any) => any
quality: State['objectData']['quality']
}
export const makeStlDownloadHandler =
({
geometry,
fileName,
type,
thunkDispatch,
quality,
ideType,
}: makeStlDownloadHandlerArgs) =>
() => {
const makeStlBlobFromMesh = flow(
(...meshes) => new Scene().add(...meshes),
(scene) => new STLExporter().parse(scene),
(stl) =>
new Blob([stl], {
type: 'text/plain',
})
)
const makeStlBlobFromGeo = flow(
(geo) => new Mesh(geo, new MeshBasicMaterial()),
(mesh) => makeStlBlobFromMesh(mesh)
)
const saveFile = (blob) => {
fileSave(blob, {
fileName,
extensions: ['.stl'],
})
}
if (geometry) {
if (
type === 'geometry' &&
(quality === 'high' || ideType === 'openscad')
) {
saveFile(makeStlBlobFromGeo(geometry))
} else if (ideType == 'jscad') {
saveFile(makeStlBlobFromMesh(...geometry))
} else {
thunkDispatch((dispatch, getState) => {
const state = getState()
const specialCadProcess = ideType === 'openscad' && 'stl'
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
quality: 'high',
specialCadProcess,
parameters: state.currentParameters,
}).then(
(result) => result && saveFile(makeStlBlobFromGeo(result.data))
)
})
}
}
}

View File

@@ -1,7 +0,0 @@
import EmojiReaction from './EmojiReaction'
export const generated = () => {
return <EmojiReaction />
}
export default { title: 'Components/EmojiReaction' }

View File

@@ -3,7 +3,7 @@ import { getActiveClasses } from 'get-active-classes'
import Popover from '@material-ui/core/Popover' import Popover from '@material-ui/core/Popover'
import { useAuth } from '@redwoodjs/auth' import { useAuth } from '@redwoodjs/auth'
import Svg from 'src/components/Svg' import Svg from 'src/components/Svg/Svg'
const emojiMenu = ['❤️', '👍', '😄', '🙌'] const emojiMenu = ['❤️', '👍', '😄', '🙌']
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌'] // const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
@@ -20,7 +20,7 @@ const EmojiReaction = ({
emotes, emotes,
userEmotes, userEmotes,
onEmote = () => {}, onEmote = () => {},
onShowPartReactions, onShowProjectReactions,
className, className,
}) => { }) => {
const { currentUser } = useAuth() const { currentUser } = useAuth()
@@ -57,42 +57,41 @@ const EmojiReaction = ({
return ( return (
<> <>
<div <div
className={getActiveClasses( className={getActiveClasses('relative overflow-hidden pt-1', className)}
'h-10 relative overflow-hidden py-4',
className
)}
> >
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100"> <div className="z-10 flex items-center h-10">
<div <div
className="h-8 w-8 relative" className="h-full w-10 mr-4"
aria-describedby={popoverId} aria-describedby={popoverId}
onClick={togglePopover} onClick={togglePopover}
> >
<button className="bg-gray-200 border-2 m-px w-full h-full border-gray-300 rounded-full flex justify-center items-center shadow-md hover:shadow-lg hover:border-indigo-200 transform hover:-translate-y-px transition-all duration-150"> <button className="bg-ch-gray-600 w-full h-full flex justify-center items-center shadow-md hover:shadow-lg transform hover:-translate-y-px transition-all duration-150 rounded">
<Svg <Svg className="w-8 text-ch-gray-300" name="dots-vertical" />
className="h-8 w-8 pt-px mt-px text-gray-500"
name="dots-vertical"
/>
</button> </button>
</div> </div>
</div>
<div className="whitespace-nowrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
{(emotes.length ? emotes : noEmotes).map((emote, i) => ( {(emotes.length ? emotes : noEmotes).map((emote, i) => (
<span <span
className={getActiveClasses( className={getActiveClasses(
'rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150 border-indigo-400', 'tracking-wide border border-transparent hover:border-ch-gray-300 h-full p-1 px-4 transform hover:-translate-y-px transition-all duration-150 flex items-center rounded',
{ border: currentUser && userEmotes?.includes(emote.emoji) } {
'bg-ch-gray-500 text-ch-gray-900':
currentUser && userEmotes?.includes(emote.emoji),
'bg-ch-gray-600': !(
currentUser && userEmotes?.includes(emote.emoji)
),
}
)} )}
style={textShadow} style={textShadow}
key={`${emote.emoji}--${i}`} key={`${emote.emoji}--${i}`}
onClick={() => handleEmojiClick(emote.emoji)} onClick={() => handleEmojiClick(emote.emoji)}
> >
<span className="text-lg pr-1">{emote.emoji}</span> <span className="text-lg pr-2">{emote.emoji}</span>
<span className="text-sm font-ropa-sans">{emote.count}</span> <span className="text-sm font-fira-code">{emote.count}</span>
</span> </span>
))} ))}
</div> </div>
<div className="whitespace-nowrap flex items-center flex-row-reverse"></div>
</div> </div>
<Popover <Popover
id={popoverId} id={popoverId}
@@ -121,7 +120,7 @@ const EmojiReaction = ({
</button> </button>
))} ))}
</div> </div>
<button className="text-gray-700" onClick={onShowPartReactions}> <button className="text-gray-700" onClick={onShowProjectReactions}>
View Reactions View Reactions
</button> </button>
</div> </div>

View File

@@ -0,0 +1,126 @@
import { useState } from 'react'
import { useIdeContext, ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
import OutBound from 'src/components/OutBound/OutBound'
import { prepareEncodedUrl, makeExternalUrl } from './helpers'
import { copyTextToClipboard } from 'src/helpers/clipboard'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { toast } from '@redwoodjs/web/toast'
const ExternalScript = () => {
const { state, thunkDispatch } = useIdeContext()
const handleRender = useRender()
const [rawUrl, setRawUrl] = useState('')
const [script, setScript] = useState('')
const [asyncState, setAsyncState] =
useState<'INIT' | 'SUCCESS' | 'ERROR' | 'LOADING'>('INIT')
const cadName = ideTypeNameMap[state.ideType]
const onPaste: React.ClipboardEventHandler<HTMLInputElement> = async ({
clipboardData,
}) => {
const url = clipboardData.getData('Text')
processUserUrl(url)
}
const onChange: React.ChangeEventHandler<HTMLInputElement> = async ({
target,
}) => setRawUrl(target.value)
const onKeyDown = async ({ key, target }) =>
key === 'Enter' && processUserUrl(target.value)
async function processUserUrl(url: string) {
setRawUrl(url)
try {
setAsyncState('LOADING')
const response = await fetch(prepareEncodedUrl(url))
if (response.status === 404) throw new Error("couldn't find script")
const script2 = await response.text()
if (script2.startsWith('<!DOCTYPE html>'))
throw new Error('got html document, not a script')
setScript(script2)
setAsyncState('SUCCESS')
} catch (e) {
setAsyncState('ERROR')
toast.error(
"We had trouble with you're URL, are you sure it was correct?"
)
}
}
const onCopyRender: React.MouseEventHandler<HTMLButtonElement> = () => {
copyTextToClipboard(makeExternalUrl(rawUrl))
thunkDispatch({ type: 'updateCode', payload: script })
setTimeout(handleRender)
}
return (
<div className="p-4">
<p className="text-sm pb-4">
Paste an external url containing a {cadName} script to generate a new
CadHub url for this resource.{' '}
<OutBound
className="underline text-gray-500"
to="https://learn.cadhub.xyz/docs/general-cadhub/external-resource-url"
>
Learn more
</OutBound>{' '}
about this feature.
</p>
{['INIT', 'ERROR'].includes(asyncState) && (
<>
<p>Paste url</p>
<input
className="p-1 text-xs rounded border border-gray-700 w-full"
value={rawUrl}
onChange={onChange}
onPaste={onPaste}
onKeyDown={onKeyDown}
/>
</>
)}
{asyncState === 'ERROR' && (
<p className="text-sm text-red-800">That didn't work, try again.</p>
)}
{asyncState === 'LOADING' && (
<div className="h-10 relative">
<div className="inset-0 absolute flex items-center justify-center">
<div className="h-6 w-6 bg-pink-600 rounded-full animate-ping"></div>
</div>
</div>
)}
{asyncState === 'SUCCESS' && (
<>
<input
value={makeExternalUrl(rawUrl).replace(/^.+:\/\//g, '')}
readOnly
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
/>
<button
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
onClick={() => copyTextToClipboard(makeExternalUrl(rawUrl))}
>
Copy URL
</button>
<div className="flex flex-col gap-2 pt-2">
<button
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
onClick={onCopyRender}
>
Copy &amp; Render
</button>
<button
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
onClick={() => {
setAsyncState('INIT')
setRawUrl('')
setScript('')
}}
>
Create another URL
</button>
</div>
</>
)}
</div>
)
}
export default ExternalScript

View File

@@ -0,0 +1,28 @@
import { makeEncodedLink } from './helpers'
import { copyTextToClipboard } from 'src/helpers/clipboard'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
const FullScriptEncoding = () => {
const { state } = useIdeContext()
const encodedLink = makeEncodedLink(state.code)
return (
<div className="p-4">
<p className="text-sm pb-4 border-b border-gray-700">
Encodes your CodeCad script into a URL so that you can share your work
</p>
<input
value={encodedLink.replace(/^.+:\/\//g, '')}
readOnly
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
/>
<button
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
onClick={() => copyTextToClipboard(encodedLink)}
>
Copy URL
</button>
</div>
)
}
export default FullScriptEncoding

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