├── .gitignore ├── LICENSE ├── README.md ├── bookstore_backend.png ├── cloudflare_end_to_end.png ├── cloudflare_summary_diagram.png ├── ecommerce.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── screenshot.png ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── Routes.tsx ├── apiCalls.ts ├── apiRtt.ts ├── common │ ├── AddToCart.tsx │ ├── PropsRoute.tsx │ ├── friendRecommendations │ │ ├── FriendRecommendations.tsx │ │ └── FriendThumb.tsx │ ├── hero │ │ ├── Hero.tsx │ │ └── hero.css │ ├── starRating │ │ ├── StarRating.tsx │ │ └── starRating.css │ ├── styles │ │ ├── common.css │ │ ├── gallery.css │ │ └── productRow.css │ └── table │ │ └── EnhancedTable.tsx ├── images │ ├── avatars │ │ ├── Brenda.png │ │ ├── Erin.png │ │ ├── Jacob.png │ │ ├── Jeff.png │ │ ├── Jennifer.png │ │ ├── John.png │ │ └── Sarah.png │ ├── bestSellers.png │ ├── bestSellers │ │ ├── burgers.png │ │ ├── italian.png │ │ ├── noodles.png │ │ ├── pancakes.png │ │ ├── pineapple.png │ │ └── umami.png │ ├── bookstore.png │ ├── hero │ │ ├── hero-cars.png │ │ ├── hero-cookbooks.png │ │ ├── hero-database.png │ │ ├── hero-fairytales.png │ │ ├── hero-home.png │ │ ├── hero-main-old.png │ │ ├── hero-main.png │ │ ├── hero-science.png │ │ └── hero-woodwork.png │ ├── pastOrders.png │ ├── screenshot.png │ ├── supportedCards.png │ ├── yourpastorders.png │ └── yourshoppingcart.png ├── index.css ├── index.tsx ├── modules │ ├── bestSellers │ │ ├── BestSellerProductRow.tsx │ │ ├── BestSellers.tsx │ │ └── bestSellersBar │ │ │ └── BestSellersBar.tsx │ ├── cart │ │ ├── CartProductRow.tsx │ │ └── ShoppingCart.tsx │ ├── category │ │ ├── CategoryGallery.tsx │ │ ├── CategoryGalleryBook.tsx │ │ ├── CategoryGalleryTeaser.tsx │ │ ├── CategoryView.tsx │ │ └── categoryNavBar │ │ │ ├── CategoryNavBar.tsx │ │ │ ├── categories.css │ │ │ └── categories.ts │ ├── checkout │ │ ├── Checkout.tsx │ │ ├── CheckoutConfirm.tsx │ │ ├── checkout.css │ │ └── checkoutForm │ │ │ ├── CheckoutForm.tsx │ │ │ └── checkoutForm.css │ ├── friends │ │ ├── FriendsBought.tsx │ │ └── ProductRow.tsx │ ├── notFound │ │ ├── NotFound.tsx │ │ └── notFound.css │ ├── pastPurchases │ │ ├── PastPurchases.tsx │ │ └── PurchasedProductRow.tsx │ ├── search │ │ ├── SearchGallery.tsx │ │ ├── SearchView.tsx │ │ └── searchBar │ │ │ ├── SearchBar.tsx │ │ │ └── searchBar.css │ └── signup │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ ├── Signup.tsx │ │ ├── home.css │ │ ├── login.css │ │ └── signup.css ├── react-app-env.d.ts └── registerServiceWorker.ts ├── tsconfig.json ├── workers-site ├── .gitignore ├── c8qls.js ├── index.js ├── init.js ├── package-lock.json ├── package.json └── router.js └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | worker/ 9 | node_modules/ 10 | .cargo-ok 11 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Macrometa CloudFlare e-commerce template app 2 | 3 | **Live Demo: https://bookstore.macrometa.io/** 4 | 5 | Macrometa-Cloudflare Bookstore Demo App is a full-stack e-commerce web application that creates a storefront (and backend) for customers to shop for "fictitious" books. 6 | 7 | ![alt text](./screenshot.png) 8 | 9 | Originally based on the AWS bookstore template app (https://github.com/aws-samples/aws-bookstore-demo-app), this demo replaces all AWS services like below 10 | 11 | - AWS DynamoDB, 12 | - AWS Neptune (Graphs), 13 | - AWS ElasticSearch (Search), 14 | - AWS Lambda 15 | - AWS Kinesis 16 | 17 | This demo uses Macrometa's geo distributed data platform which provides a `K/V store`, `DynamoDB compatible document database`, `graph database`, `streams` and `stream processing` along with Cloudflare `edgeworkers` for the globally distributed functions as a service. 18 | 19 | Unlike typical cloud platforms like AWS, where the backend stack runs in a single region, Macrometa and Cloudflare let you build `stateful distributed microservices that run in 100s of regions around the world concurrently`. The application logic runs in cloudflare's low latency function as a service runtime on cloudflare PoPs and make stateful data requests to the closest Macrometa region. End to end latency for `P90 is < 55ms` from almost everywhere in the world. 20 | 21 | As a user of the demo, you can browse and search for books, look at recommendations and best sellers, manage your cart, checkout, view your orders, and more. 22 | 23 | ## GDN Tenant Account 24 | 25 | | **Federation** | **Fabric** | **Email** | 26 | | ----------------------------------------------------- | ------------- | ---------------------- | 27 | | [Play](https://play.macrometa.io/) | book_store | demo@macrometa.io | 28 | 29 | ## Architecture 30 | 31 | ![Cloudflare Arch](./cloudflare_summary_diagram.png) 32 | 33 | ## Data & Control Flows 34 | 35 | ![Cloudflare End to End](./cloudflare_end_to_end.png) 36 | 37 | ## Details 38 | 39 | ### Frontend 40 | 41 | - Frontend is a Reactjs application which is hosted using Cloudflare. 42 | - Web assets are stored on Cloudflare's KV store. 43 | 44 | ### Backend 45 | 46 | The core of backend infrastructure consists of Macrometa Document store(DB), Macrometa Edge store(DB), Macrometa Views(search), Macrometa Stream Workers, Macrometa Graphs and Cloudflare workers. Cloudflare workers issue C8QLs to talk with the GDN network. 47 | 48 | The application leverages Macrometa GDN document store to store all the data for books, orders, the checkout cart and users. When new purchases or new users are added the corresponding Macrometa Edge collection is also updated. These Edge collections along with Document collection acting as vertices are used by the Macrometa Graphs to generate recommendations for the users. When new purchases are added Macrometa Stream Workers also update the BestSellers Collection store in realtime from which the best sellers leaderboard is generated. 49 | 50 | ![Bookstore Backend](./bookstore_backend.png) 51 | 52 | **Catalog, Cart, Orders:** 53 | 54 | This is implemented using `document collections` functionality in Macrometa GDN 55 | 56 | | Entity | Collection Name | Collection Type | Comment | 57 | | ------- | --------------- | --------------- | ------------------------------------------ | 58 | | Catalog | BooksTable | document | Collection of the available books. | 59 | | Cart | CartTable | document | Books customers have addded in their cart. | 60 | | Orders | OrdersTable | document | Past orders of a customer. | 61 | 62 | **Recommendations:** 63 | 64 | This is implemented using `graphs` functionality in Macrometa GDN. Each node in the graph is a `vertex` and the links connecting the nodes are `edges`. Both `vertex` and `edges` are document collections. The `edges` require two additional mandatory indexes i.e., `_from` and `_to`. 65 | 66 | | Entity | Collection Name | Collection Type | Comment | 67 | | -------- | --------------- | --------------- | -------------------------------------------- | 68 | | Friends | Friend | edge | Edge collection to capture friend relations. | 69 | | Purchase | Purchased | edge | Edge collection to capture purchases. | 70 | | Users | UserTable | vertex | Document collection of available users. | 71 | | Catalog | BooksTable | vertex | Collection of the available books. | 72 | | Social | UserSocialGraph | graph | User social graph | 73 | 74 | **Search:** 75 | 76 | Search is implemented using `views` functionality in Macrometa GDN. Search matches on the `category` or the `name` of book in `BooksTable` with phrase matching. 77 | 78 | | Entity | Collection Name | Collection Type | Comment | 79 | | ------ | --------------- | --------------- | ------------------------------------- | 80 | | Find | findBooks | view | The view which is queried for search. | 81 | 82 | **Top Sellers List:** 83 | 84 | This is implemented using `streams` and `stream processing` functionality in Macrometa. 85 | 86 | | Entity | Name | Type | Comment | 87 | | ---------- | ---------------- | ------------- | -------------------------------------------------------------------- | 88 | | BestSeller | UpdateBestseller | stream worker | Stream worker to process orders and update best sellers in realtime. | 89 | | BestSeller | BestsellersTable | document | Collection to store best sellers. | 90 | 91 | **Indexes:** 92 | 93 | Create persistent indexes on the collection for the corresponding attributes 94 | 95 | | **Collection** | **Attribute** | 96 | | ---------------- | ------------------------------------ | 97 | | BestsellersTable | `quantity` | 98 | | CartTable | single index on `customerId, bookId` | 99 | | BooksTable | `category` | 100 | | friend | N/A | 101 | | OrdersTable | `customerId` | 102 | | UsersTable | `customerId` | 103 | 104 | ## API Details 105 | 106 | Below are the list of APIs being used. 107 | 108 | **Books (Macrometa Docuemnt Store DB)** 109 | 110 | - GET /books (ListBooks) 111 | - GET /books/{:id} (GetBook) 112 | 113 | **Cart (Macrometa Docuemnt Store DB)** 114 | 115 | - GET /cart (ListItemsInCart) 116 | - POST /cart (AddToCart) 117 | - PUT /cart (UpdateCart) 118 | - DELETE /cart (RemoveFromCart) 119 | - GET /cart/{:bookId} (GetCartItem) 120 | 121 | **Orders (Macrometa Docuemnt Store DB)** 122 | 123 | - GET /orders (ListOrders) 124 | - POST /orders (Checkout) 125 | 126 | **Best Sellers (Macrometa Docuemnt Store DB)** 127 | 128 | - GET /bestsellers (GetBestSellers) 129 | 130 | **Recommendations (Macrometa Graphs)** 131 | 132 | - GET /recommendations (GetRecommendations) 133 | - GET /recommendations/{bookId} (GetRecommendationsByBook) 134 | 135 | **Search (Macrometa Views)** 136 | 137 | - GET /search (Search) 138 | 139 | ## Queries 140 | 141 | C8QLs are used by the Cloudflare workers to communicate with Macrometa GDN. 142 | 143 | **signup**: 144 | 145 | ```js 146 | INSERT {_key: @username, password: @passwordHash, customerId: @customerId} INTO UsersTable 147 | ``` 148 | 149 | **signin**: 150 | 151 | ```js 152 | FOR user in UsersTable FILTER user._key == @username AND user.password == @passwordHash RETURN user.customerId 153 | ``` 154 | 155 | **AddFriends**: 156 | 157 | ```js 158 | LET otherUsers = (FOR users in UsersTable FILTER users._key != @username RETURN users) 159 | FOR user in otherUsers 160 | INSERT { _from: CONCAT("UsersTable/",@username), _to: CONCAT("UsersTable/",user._key) } INTO friend 161 | ``` 162 | 163 | **ListBooks**: 164 | 165 | ```js 166 | FOR book IN BooksTable RETURN book 167 | ``` 168 | 169 | OR 170 | 171 | ```js 172 | FOR book IN BooksTable filter book.category == @category RETURN book 173 | ``` 174 | 175 | **GetBook**: 176 | 177 | ```js 178 | FOR book in BooksTable FILTER book._key == @bookId RETURN book 179 | ``` 180 | 181 | **ListItemsInCart**: 182 | 183 | ```js 184 | FOR item IN CartTable FILTER item.customerId == @customerId 185 | FOR book in BooksTable FILTER book._key == item.bookId 186 | RETURN {order: item, book: book} 187 | ``` 188 | 189 | **AddToCart**: 190 | 191 | ```js 192 | UPSERT { _key: CONCAT_SEPARATOR(":", @customerId, @bookId) } 193 | INSERT { _key: CONCAT_SEPARATOR(":", @customerId, @bookId),customerId: @customerId, bookId: @bookId, quantity: @quantity, price: @price } 194 | UPDATE { quantity: OLD.quantity + @quantity } IN CartTable 195 | ``` 196 | 197 | **UpdateCart**: 198 | 199 | ```js 200 | FOR item IN CartTable UPDATE {_key: CONCAT_SEPARATOR(":", @customerId, @bookId),quantity: @quantity} IN CartTable 201 | ``` 202 | 203 | **RemoveFromCart**: 204 | 205 | ```js 206 | REMOVE {_key: CONCAT_SEPARATOR(":", @customerId, @bookId)} IN CartTabl 207 | ``` 208 | 209 | **GetCartItem**: 210 | 211 | ```js 212 | FOR item IN CartTable FILTER item.customerId == @customerId AND item.bookId == @bookId RETURN item 213 | ``` 214 | 215 | **ListOrders**: 216 | 217 | ```js 218 | FOR item IN OrdersTable FILTER item.customerId == @customerId RETURN item 219 | ``` 220 | 221 | **Checkout**: 222 | 223 | ```js 224 | LET items = (FOR item IN CartTable FILTER item.customerId == @customerId RETURN item) 225 | LET books = (FOR item in items 226 | FOR book in BooksTable FILTER book._key == item.bookId return {bookId:book._key ,author: book.author,category:book.category,name:book.name,price:book.price,rating:book.rating,quantity:item.quantity}) 227 | INSERT {_key: @orderId, customerId: @customerId, books: books, orderDate: @orderDate} INTO OrdersTable 228 | FOR item IN items REMOVE item IN CartTable 229 | ) 230 | 231 | INSERT {_key: @orderId, customerId: @customerId, fashionItems: fashionItems, orderDate: @orderDate} INTO OrdersTable 232 | ``` 233 | 234 | **AddPurchased**: 235 | 236 | ```js 237 | LET order = first(FOR order in OrdersTable FILTER order._key == @orderId RETURN {customerId: order.customerId, books: order.books}) 238 | LET customerId = order.customerId 239 | LET userId = first(FOR user IN UsersTable FILTER user.customerId == customerId RETURN user._id) 240 | LET books = order.books 241 | FOR book IN books 242 | INSERT {_from: userId, _to: CONCAT("BooksTable/",book.bookId)} INTO purchased 243 | ``` 244 | 245 | **GetBestSellers**: 246 | 247 | ```js 248 | FOR bestseller in BestsellersTable 249 | SORT bestseller.quantity DESC 250 | FOR book in BooksTable 251 | FILTER bestseller._key == book._key LIMIT 20 RETURN book 252 | ``` 253 | 254 | **GetRecommendations**: 255 | 256 | ```js 257 | LET userId = first(FOR user in UsersTable FILTER user.customerId == @customerId return user._id) 258 | FOR user IN ANY userId friend 259 | FOR books IN OUTBOUND user purchased 260 | RETURN DISTINCT books 261 | ``` 262 | 263 | **GetRecommendationsByBook**: 264 | 265 | ```js 266 | LET userId = first(FOR user in UsersTable FILTER user.customerId == @customerId return user._id) 267 | LET bookId = CONCAT("BooksTable/",@bookId) 268 | FOR friendsPurchased IN INBOUND bookId purchased 269 | FOR user IN ANY userId friend 270 | FILTER user._key == friendsPurchased._key 271 | RETURN user 272 | ``` 273 | 274 | **Search** 275 | 276 | ```js 277 | FOR doc IN findBooks 278 | SEARCH PHRASE(doc.name, @search, "text_en") OR PHRASE(doc.author, @search, "text_en") OR PHRASE(doc.category, @search, "text_en") 279 | SORT BM25(doc) desc 280 | RETURN doc 281 | ``` 282 | 283 | ## Macrometa Views 284 | 285 | Search functionality is powered by Macrometa Views. This is saved as `findFashionItems` with below config: 286 | 287 | ```json 288 | { 289 | "links": { 290 | "FashionItemsTable": { 291 | "analyzers": ["text_en"], 292 | "fields": {}, 293 | "includeAllFields": true, 294 | "storeValues": "none", 295 | "trackListPositions": false 296 | } 297 | }, 298 | "primarySort": [] 299 | } 300 | ``` 301 | 302 | ## Stream Workers 303 | 304 | Best seller leader board made with `BestsellersTable` which is updated with each new purchase via the `UpdateBestseller` stream worker 305 | 306 | ```js 307 | @App:name("UpdateBestseller") 308 | @App:description("Updates BestsellerTable when a new order comes in the OrdersTable") 309 | 310 | define function getBookQuantity[javascript] return int { 311 | const prevQuantity = arguments[0]; 312 | const nextQuantity = arguments[1]; 313 | 314 | let newQuantity = nextQuantity; 315 | if(prevQuantity){ 316 | newQuantity = prevQuantity + nextQuantity; 317 | } 318 | return newQuantity; 319 | }; 320 | 321 | @source(type='c8db', collection='OrdersTable', @map(type='passThrough')) 322 | define stream OrdersTable (_json string); 323 | 324 | @sink(type='c8streams', stream='BestsellerIntermediateStream', @map(type='json')) 325 | define stream BestsellerIntermediateStream (bookId string, quantity int); 326 | 327 | @store(type = 'c8db', collection='BestsellersTable') 328 | define table BestsellersTable (_key string, quantity int); 329 | 330 | select json:getString(jsonElement, '$.bookId') as bookId, json:getInt(jsonElement, '$.quantity') as quantity 331 | from OrdersTable#json:tokenizeAsObject(_json, "$.books[*]") 332 | insert into BestsellerIntermediateStream; 333 | 334 | select next.bookId as _key, getBookQuantity(prev.quantity, next.quantity) as quantity 335 | from BestsellerIntermediateStream as next 336 | left outer join BestsellersTable as prev 337 | on next.bookId == prev._key 338 | update or insert into BestsellersTable 339 | set BestsellersTable.quantity = quantity, BestsellersTable._key = _key 340 | on BestsellersTable._key == _key; 341 | 342 | ``` 343 | 344 | ## Development Details 345 | 346 | ### Notes 347 | 348 | - Book images are stored in Cloudflare KV under `Book_IMAGES`. 349 | - Root of the repo contains the code for the UI which is in Reactjs 350 | - `workers-site` folder contains the backend part. This is responsible for both serving the web assets and also making calls to Macrometa GDN. 351 | - Calls with `/api/` are treated as calls which want to communicate with Macrometa GDN, others are understood to be calls for the web assets. 352 | - `handleEvent(index.js)` get the request and calls the appropriate handler based on the regex of the request with the help of a simple router function defined in `router.js`. 353 | - `c8qls.js` contains the queries (C8QL). These are executed by calling Macrometa GDN `/cursor` API. The `bind variables` in the body of the request are the parameters to the queries. 354 | 355 | # Project setup 356 | 357 | ## Installing workers CLI 358 | 359 | There are multiple ways to install the workers CLI. Official docs say it to install via [npm](https://developers.cloudflare.com/workers/learning/getting-started#2-install-the-workers-cli) or [cargo](https://github.com/cloudflare/wrangler#install-with-cargo). 360 | Additionally the binary can also be installed manually. Details of which can be found [here](https://developer.aliyun.com/mirror/npm/package/@granjef3/wrangler) under the `Manual Install` section - I personally have the binaries. 361 | 362 | It is advisable to have `npm` installed via `nvm` to avoid getting into issues when installing global packages. Additional details can be found in their [github repo](https://github.com/cloudflare/wrangler#install-with-npm). 363 | 364 | ## Configuring the project for deployment 365 | 366 | ### Obtaining your API token 367 | 368 | We will need the Macrometa API token to be able to configure the CLI. Please signup for a macrometa account for the token, or create your own by following the docs if you already have an account [here](https://developers.cloudflare.com/workers/learning/getting-started#6b-obtaining-your-api-token-or-global-api-key) 369 | 370 | ### Configuring Wrangler with your credentials 371 | 372 | Run `wrangler config` and enter the above API token when asked for. More details can be found [here](https://developers.cloudflare.com/workers/learning/getting-started#6c-configuring-wrangler-with-your-credentials) 373 | 374 | ## Configuring your project 375 | 376 | `wrangler.toml` already has the configurations. 377 | 378 | > Provide a `C8_API_KEY` with a correct API key before proceeding. 379 | 380 | `vars` provides the environment variable we use in the workers itself. They include: 381 | 382 | 1. `DC_LIST`: for stream app init 383 | 2. `C8_URL`: GDN federation URL 384 | 3. `C8_FABRIC`: fabric name 385 | 4. `C8_API_KEY`: API key of the tenant being used 386 | 387 | ## Publishing your project 388 | 389 | Make sure to run `npm i` on the project's root to install the necessary dependencies. 390 | 391 | ## Building the UI 392 | 393 | If there are changes to the UI code then first run `npm run build` to make the UI build, else you can directly proceed with publishing. 394 | 395 | ## Publishing 396 | 397 | Run `wrangler publish` and it will deploy your worker along with the static files used by the UI. 398 | 399 | # Initialising the collections and streamapp 400 | 401 | Once the worker is deployed, execute the following curl: 402 | 403 | ``` 404 | curl 'https://bookstore.macrometadev.workers.dev/api/init' -H 'authority: bookstore.macrometadev.workers.dev' -H 'sec-ch-ua: "Chromium";v="86", "\"Not\\A;Brand";v="99", "Google Chrome";v="86"' -H 'x-customer-id: null' -H 'sec-ch-ua-mobile: ?0' -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36' -H 'content-type: text/plain;charset=UTF-8' -H 'accept: */*' -H 'origin: https://bookstore.macrometadev.workers.dev' -H 'sec-fetch-site: same-origin' -H 'sec-fetch-mode: cors' -H 'sec-fetch-dest: empty' -H 'referer: https://bookstore.macrometadev.workers.dev/signup' -H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' -H 'cookie: __cfduid=de7d15f3918fe96a07cf5cedffdecba081601555750' --data-binary '{}' --compressed 405 | ``` 406 | 407 | This will create all the collections and dummy data for you. 408 | 409 | > Note: This will only populate if the collection or stream app is not already present. If it does it wont create the dummy data, even if the collection is empty. So best to delete the collection if you want it to be populated by the curl. 410 | 411 | ### After you run the demo do the following: 412 | 413 | 1. Now login to the tenant and activate the stream app. 414 | 2. Edit and save the view with the correct data if not initialised properly. Details can be found in `init.js` 415 | -------------------------------------------------------------------------------- /bookstore_backend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/bookstore_backend.png -------------------------------------------------------------------------------- /cloudflare_end_to_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/cloudflare_end_to_end.png -------------------------------------------------------------------------------- /cloudflare_summary_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/cloudflare_summary_diagram.png -------------------------------------------------------------------------------- /ecommerce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/ecommerce.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c8-bookstore-demo-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@types/graphql": "^14.0.5", 9 | "@types/jest": "^23.3.10", 10 | "@types/node": "^10.12.12", 11 | "@types/react": "^16.7.12", 12 | "@types/react-bootstrap": "^0.32.15", 13 | "@types/react-dom": "^16.0.11", 14 | "@types/react-router-bootstrap": "^0.24.5", 15 | "@types/react-router-dom": "^4.3.1", 16 | "bootstrap": "^3.3.7", 17 | "react": "^16.5.0", 18 | "react-bootstrap": "^0.32.4", 19 | "react-dom": "^16.5.0", 20 | "react-router-bootstrap": "^0.24.4", 21 | "react-router-dom": "^4.3.1", 22 | "react-scripts": "^3.0.1", 23 | "typescript": "^3.2.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "GENERATE_SOURCEMAP=false REACT_APP_BACKEND=\"https://bookstore.macrometa.io\" react-scripts build", 28 | "test": "react-scripts test --env=jsdom", 29 | "eject": "react-scripts eject" 30 | }, 31 | "browserslist": [ 32 | ">0.2%", 33 | "not dead", 34 | "not ie <= 11", 35 | "not op_mini all" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/screenshot.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | margin-top: 15px; 3 | } 4 | .app-restyling { 5 | margin: 2vh 2vw 2vw !important; 6 | } 7 | .App .navbar-brand { 8 | font-weight: bold; 9 | } 10 | 11 | .App .orange { 12 | color: #2eadde; 13 | } 14 | 15 | .App .white { 16 | color: #fff; 17 | } 18 | 19 | .shopping-icon-container { 20 | background-color: #2eadde; 21 | padding: 2px 12px 0 12px; 22 | border-radius: 14px; 23 | margin-right: -24px; 24 | } 25 | 26 | .shopping-icon-container .count { 27 | margin: 0 0 0 4px; 28 | } 29 | 30 | .line-height-24 { 31 | line-height: 24px; 32 | } 33 | 34 | .navbar-default .navbar-nav > .open > a, 35 | .navbar-default .navbar-nav > .active > a:active { 36 | background-color: transparent !important; 37 | background-image: none !important; 38 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 39 | } 40 | 41 | .navbar-default .navbar-nav > .open > a, 42 | .navbar-default .navbar-nav > .active > a:focus { 43 | background-color: transparent !important; 44 | background-image: none !important; 45 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 46 | } 47 | .navbar-default .navbar-nav > .open > a, 48 | .navbar-default .navbar-nav > .active > a:focus-within { 49 | background-color: transparent !important; 50 | background-image: none !important; 51 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 52 | } 53 | 54 | .navbar-default .navbar-nav > .open > a, 55 | .navbar-default .navbar-nav > .active > a:visited { 56 | background-color: transparent !important; 57 | background-image: none !important; 58 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 59 | } 60 | 61 | .navbar-default .navbar-nav > .open > a, 62 | .navbar-default .navbar-nav > .active > a { 63 | background-color: transparent !important; 64 | background-image: none !important; 65 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 66 | } 67 | 68 | .checkbox-styling { 69 | padding-top: 7px; 70 | margin-left: -20px; 71 | /* background-color: #2eadde; */ 72 | } 73 | .navbar-items-font-style { 74 | color: #2eadde; 75 | padding-top: 2px !important; 76 | line-height: 24px; 77 | font-size: 16px; 78 | font-weight: bolder; 79 | margin-left: 2px; 80 | } 81 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Auth } from "./apiCalls"; 2 | import React, { Component, Fragment } from "react"; 3 | import { LinkContainer } from "react-router-bootstrap"; 4 | import { Link, withRouter } from "react-router-dom"; 5 | import { 6 | ButtonToolbar, 7 | Form, 8 | Nav, 9 | Navbar, 10 | NavItem, 11 | ToggleButton, 12 | ToggleButtonGroup, 13 | } from "react-bootstrap"; 14 | import "./App.css"; 15 | import { Routes } from "./Routes"; 16 | 17 | import bookstore from "./images/bookstore.png"; 18 | import SearchBar from "./modules/search/searchBar/SearchBar"; 19 | 20 | interface AppProps { 21 | history: any; 22 | } 23 | 24 | interface AppState { 25 | isAuthenticated: boolean; 26 | isAuthenticating: boolean; 27 | showNetworkLatency: boolean; 28 | showLatestNetworkLatencyValue: string; 29 | } 30 | 31 | class App extends Component { 32 | private performanceButton: React.RefObject; 33 | 34 | constructor(props: AppProps) { 35 | super(props); 36 | 37 | this.state = { 38 | isAuthenticated: false, 39 | isAuthenticating: true, 40 | showNetworkLatency: false, 41 | showLatestNetworkLatencyValue: "50 ms", 42 | }; 43 | this.performanceButton = React.createRef(); 44 | document.title = "Edge Commerce Demo"; 45 | } 46 | 47 | async componentDidMount() { 48 | if (!sessionStorage.getItem("responseTime")) { 49 | sessionStorage.setItem("responseTime", JSON.stringify([])); 50 | } 51 | document.addEventListener("click", this.handleOutsideClick); 52 | try { 53 | if (await Auth.currentSession()) { 54 | this.userHasAuthenticated(true); 55 | } 56 | } catch (e) { 57 | if (e !== "No current user") { 58 | console.error(e); 59 | } 60 | } 61 | 62 | this.setState({ isAuthenticating: false }); 63 | } 64 | handleOutsideClick = (event: any) => { 65 | const closestParent = event.target.closest("#paper-id"); 66 | 67 | const dropdownClosest = event.target.closest("#menu-"); 68 | if ( 69 | !event.target.id.includes("category-nav-bar") && 70 | !closestParent && 71 | event.target.tagName.toLowerCase() !== "body" && 72 | !dropdownClosest && 73 | this.performanceButton && 74 | this.performanceButton.current && 75 | !this.performanceButton.current.contains(event.target) 76 | ) { 77 | this.setState({ showNetworkLatency: false }); 78 | } 79 | }; 80 | componentWillUnmount() { 81 | window.removeEventListener("click", this.handleOutsideClick); 82 | } 83 | userHasAuthenticated = (authenticated: boolean) => { 84 | const networkLatency = JSON.parse( 85 | sessionStorage.getItem("responseTime") || "[]" 86 | ); 87 | const networkLatencyValue = 88 | networkLatency && 89 | networkLatency.length > 0 && 90 | networkLatency[networkLatency.length - 1].Time; 91 | this.setState({ 92 | isAuthenticated: authenticated, 93 | showLatestNetworkLatencyValue: networkLatencyValue, 94 | }); 95 | }; 96 | 97 | handleLogout = async () => { 98 | await Auth.signOut(); 99 | 100 | this.userHasAuthenticated(false); 101 | this.props.history.push("/"); 102 | }; 103 | 104 | renderNetworkLatency = async () => { 105 | const networkLatency = JSON.parse( 106 | sessionStorage.getItem("responseTime") || "[]" 107 | ); 108 | const networkLatencyValue = 109 | networkLatency && 110 | networkLatency.length > 0 && 111 | networkLatency[networkLatency.length - 1].Time; 112 | this.setState((prevState) => ({ 113 | showNetworkLatency: !prevState.showNetworkLatency, 114 | showLatestNetworkLatencyValue: networkLatencyValue, 115 | })); 116 | }; 117 | 118 | showLoggedInBar = () => ( 119 | 120 | 121 | 122 | 123 | 124 | 125 | 129 | Past orders 130 | 131 | 132 | 133 | 134 | 135 | 139 | Bestsellers 140 | 141 | 142 | 143 | 144 | 148 | Log out 149 | 150 | 151 | 152 | 153 |
154 | 158 |
159 |
160 |
161 |
162 | ); 163 | 164 | showLoggedOutBar = () => ( 165 | 166 | 167 | 168 | 172 | Log in 173 | 174 | 175 | 176 | 177 | ); 178 | 179 | render() { 180 | const childProps = { 181 | isAuthenticated: this.state.isAuthenticated, 182 | userHasAuthenticated: this.userHasAuthenticated, 183 | showNetworkLatency: this.state.showNetworkLatency, 184 | }; 185 | 186 | return ( 187 | !this.state.isAuthenticating && ( 188 |
189 | 199 | 200 | 201 | 202 | 203 | bookstore EDGE COMMERCE DEMO 204 | 205 | 206 | 207 | 208 | 209 | {this.state.isAuthenticated ? ( 210 | <> 211 | 212 | 228 | {/* 232 | Latency : 233 | 234 | 239 | {this.state.showLatestNetworkLatencyValue} 240 | */} 241 | 242 | 243 | ) : null} 244 | 245 | 250 | 251 | 252 | 257 |
258 | ) 259 | ); 260 | } 261 | } 262 | 263 | export default withRouter(App as any); 264 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import Checkout from "./modules/checkout/Checkout"; 4 | import CheckoutConfirm from "./modules/checkout/CheckoutConfirm"; 5 | import Home from "./modules/signup/Home"; 6 | import Login from "./modules/signup/Login"; 7 | import NotFound from "./modules/notFound/NotFound"; 8 | import Signup from "./modules/signup/Signup"; 9 | import CategoryView from "./modules/category/CategoryView"; 10 | import ShoppingCart from "./modules/cart/ShoppingCart"; 11 | import PastPurchases from "./modules/pastPurchases/PastPurchases"; 12 | import BestSellers from "./modules/bestSellers/BestSellers"; 13 | import SearchView from "./modules/search/SearchView"; 14 | import PropsRoute from "./common/PropsRoute"; 15 | 16 | interface RouteProps { 17 | isAuthenticated: boolean; 18 | userHasAuthenticated: (authenticated: boolean) => void; 19 | showNetworkLatency: boolean; 20 | } 21 | 22 | export const Routes: React.SFC = (childProps) => ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | -------------------------------------------------------------------------------- /src/apiCalls.ts: -------------------------------------------------------------------------------- 1 | import { apiRtt } from "./apiRtt"; 2 | 3 | const CUSTOMER_ID = "customerId"; 4 | 5 | const getCustomerId = () => sessionStorage.getItem(CUSTOMER_ID); 6 | 7 | const setCustomerId = (customerId: string) => 8 | sessionStorage.setItem(CUSTOMER_ID, customerId); 9 | 10 | const getOptions = (opts: any) => ({ 11 | ...opts, 12 | headers: { "X-Customer-Id": getCustomerId() }, 13 | }); 14 | 15 | const fetchWrapper = async (url: string, options: any) => { 16 | const apiUrl = `./api${url}`; 17 | 18 | const res = await fetch(apiUrl, options); 19 | if (res.ok) { 20 | return res.json(); 21 | } else { 22 | throw res; 23 | } 24 | }; 25 | 26 | const Auth = { 27 | currentSession: async function () { 28 | return await fetchWrapper("/whoami", getOptions({ method: "GET" })); 29 | }, 30 | signOut: function () { 31 | sessionStorage.setItem(CUSTOMER_ID, ""); 32 | return true; 33 | }, 34 | signIn: async function (email: string, password: string) { 35 | try { 36 | const data = await fetchWrapper( 37 | "/signin", 38 | getOptions({ 39 | method: "POST", 40 | body: JSON.stringify({ username: email, password }), 41 | }) 42 | ); 43 | const customerId = data.message[0]; 44 | setCustomerId(customerId); 45 | } catch (e) { 46 | console.error(e); 47 | return Promise.reject(e); 48 | } 49 | }, 50 | signUp: function (email: string, password: string) { 51 | return fetchWrapper( 52 | "/signup", 53 | getOptions({ 54 | method: "POST", 55 | body: JSON.stringify({ username: email, password }), 56 | }) 57 | ); 58 | }, 59 | currentUserInfo: function () {}, 60 | confirmSignUp: function () {}, 61 | confirmSignIn: function () {}, 62 | }; 63 | 64 | const API = { 65 | get: async function (key: string, path: string, extra: any) { 66 | const response = await fetchWrapper(path, getOptions({ method: "GET" })); 67 | apiRtt("GET"); 68 | return response; 69 | }, 70 | post: async function (key: string, path: string, data: any) { 71 | const response = await fetchWrapper( 72 | path, 73 | getOptions({ method: "POST", body: JSON.stringify(data.body) }) 74 | ); 75 | apiRtt("POST"); 76 | return response; 77 | }, 78 | put: async function (key: string, path: string, data: any) { 79 | const response = await fetchWrapper( 80 | path, 81 | getOptions({ method: "PUT", body: JSON.stringify(data.body) }) 82 | ); 83 | apiRtt("PUT"); 84 | return response; 85 | }, 86 | del: async function (key: string, path: string, data: any) { 87 | const response = await fetchWrapper( 88 | path, 89 | getOptions({ method: "DELETE", body: JSON.stringify(data.body) }) 90 | ); 91 | apiRtt("DELETE"); 92 | return response; 93 | }, 94 | }; 95 | 96 | export { Auth, API }; 97 | -------------------------------------------------------------------------------- /src/apiRtt.ts: -------------------------------------------------------------------------------- 1 | export const apiRtt = (method: string) => { 2 | const BACKEND = process.env.REACT_APP_BACKEND; 3 | let performances: any = performance 4 | .getEntriesByType("resource") 5 | .filter((item: any) => item.initiatorType === "fetch"); 6 | 7 | const performanceRttArray = JSON.parse( 8 | sessionStorage.getItem("responseTime") || "[]" 9 | ); 10 | 11 | const newArray = []; 12 | for (let i = 0; i < performances.length; i++) { 13 | let duplicate = performanceRttArray.find((performanceItem: any) => { 14 | const performanceTime = 15 | Math.round(performances[i].responseEnd - performances[i].fetchStart) + 16 | " ms"; 17 | return ( 18 | performanceItem.Name === performances[i].name && 19 | (performanceItem.Time === performanceTime || 20 | performanceItem.Time > performanceTime) && 21 | performanceItem.URL === performances[i].transferSize + " B" 22 | ); 23 | }); 24 | if (typeof duplicate == "undefined") { 25 | let responseTimeValue = {}; 26 | responseTimeValue = { 27 | Name: performances[i].name, 28 | Status: "200", 29 | Path: performances[i].name.split(BACKEND)[1], 30 | Time: 31 | Math.round(performances[i].responseEnd - performances[i].fetchStart) + 32 | " ms", 33 | Method: method, 34 | URL: performances[i].transferSize + " B", 35 | }; 36 | newArray.push(responseTimeValue); 37 | } 38 | } 39 | 40 | let responseTimeArray = [...performanceRttArray, ...newArray]; 41 | 42 | if (responseTimeArray.length > 100) { 43 | const difference = Math.abs(responseTimeArray.length - 100); 44 | responseTimeArray.splice(0, difference); 45 | } 46 | sessionStorage.setItem("responseTime", JSON.stringify(responseTimeArray)); 47 | }; 48 | -------------------------------------------------------------------------------- /src/common/AddToCart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { API } from "../apiCalls"; 3 | import { Redirect } from "react-router"; 4 | import { Glyphicon } from "react-bootstrap"; 5 | 6 | interface AddToCartProps { 7 | bookId: string; 8 | price: number; 9 | variant?: string; 10 | } 11 | 12 | interface AddToCartState { 13 | loading: boolean; 14 | toCart: boolean; 15 | buttonText: string; 16 | } 17 | 18 | class AddToCart extends React.Component { 19 | constructor(props: AddToCartProps) { 20 | super(props); 21 | 22 | this.state = { 23 | loading: false, 24 | toCart: false, 25 | buttonText: `Add to cart`, 26 | }; 27 | } 28 | 29 | onAddToCart = async () => { 30 | this.setState({ loading: true }); 31 | // let bookInCart = await API.get("cart", `/cart/${this.props.bookId}`, null); 32 | // bookInCart = bookInCart[0]; 33 | // if the book already exists in the cart, increase the quantity 34 | // if (bookInCart) { 35 | // API.put("cart", "/cart", { 36 | // body: { 37 | // bookId: this.props.bookId, 38 | // quantity: bookInCart.quantity + 1, 39 | // }, 40 | // }).then(() => { 41 | // alert("Item added successfully to the cart"); 42 | // // this.setState({ 43 | // // toCart: true, 44 | // // }); 45 | // }); 46 | // } 47 | 48 | // if the book does not exist in the cart, add it 49 | // else { 50 | API.post("cart", "/cart", { 51 | body: { 52 | bookId: this.props.bookId, 53 | price: this.props.price, 54 | quantity: 1, 55 | }, 56 | }).then(() => { 57 | this.setState({ loading: false, buttonText: "Added" }); 58 | // alert("Item added successfully to the cart"); 59 | // this.setState({ 60 | // toCart: true, 61 | // }); 62 | }); 63 | }; 64 | // }; 65 | 66 | getVariant = () => { 67 | let style = "btn btn-black"; 68 | return this.props.variant && this.props.variant === "center" 69 | ? style + ` btn-black-center` 70 | : style + ` pull-right`; 71 | }; 72 | 73 | render() { 74 | // if (this.state.toCart) return ; 75 | 76 | return ( 77 | 90 | ); 91 | } 92 | } 93 | 94 | export default AddToCart; 95 | -------------------------------------------------------------------------------- /src/common/PropsRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route } from "react-router"; 3 | 4 | //@ts-ignore 5 | export default ({ component: C, props: cProps, ...rest }) => 6 | } />; -------------------------------------------------------------------------------- /src/common/friendRecommendations/FriendRecommendations.tsx: -------------------------------------------------------------------------------- 1 | import { API } from '../../apiCalls'; 2 | import React from 'react'; 3 | import { FriendThumb } from './FriendThumb'; 4 | 5 | interface FriendRecommendationsProps { 6 | bookId: string; 7 | } 8 | 9 | interface FriendRecommendationsState { 10 | friends: any[]; 11 | } 12 | 13 | class FriendRecommendations extends React.Component { 14 | constructor(props: FriendRecommendationsProps) { 15 | super(props); 16 | 17 | this.state = { 18 | friends: [] 19 | }; 20 | } 21 | 22 | getFriends = () => { 23 | return API.get("recommendations", `/recommendations/${this.props.bookId}`, null); 24 | } 25 | 26 | async componentDidMount() { 27 | try { 28 | const friends = await this.getFriends(); 29 | this.setState({ friends }); 30 | } catch (e) { 31 | console.error(e); 32 | } 33 | } 34 | 35 | render() { 36 | // No recommendations to show 37 | if (!this.state.friends[0]) { 38 | return
39 | } 40 | 41 | const numFriendsPurchased = this.state.friends.length; 42 | const friends = this.state.friends; 43 | return ( 44 |
45 |
Friends who bought this book
46 |

47 | {friends.slice(0, 3).map((friend: any) => )} 48 | {numFriendsPurchased > 3 && {` +${numFriendsPurchased - 3} ${(numFriendsPurchased - 3) > 1 ? "others" : "other"}`}} 49 |

50 |
51 | ); 52 | } 53 | } 54 | 55 | export default FriendRecommendations; -------------------------------------------------------------------------------- /src/common/friendRecommendations/FriendThumb.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Brenda from "../../images/avatars/Brenda.png"; 4 | import Erin from "../../images/avatars/Erin.png"; 5 | import Jacob from "../../images/avatars/Jacob.png"; 6 | import Jeff from "../../images/avatars/Jeff.png"; 7 | import Jennifer from "../../images/avatars/Jennifer.png"; 8 | import John from "../../images/avatars/John.png"; 9 | import Sarah from "../../images/avatars/Sarah.png"; 10 | 11 | const friends = [Brenda, Erin, Jacob, Jeff, Jennifer, John, Sarah]; 12 | 13 | interface FriendThumbProps {} 14 | 15 | export class FriendThumb extends React.Component { 16 | render() { 17 | const randomFriend = friends[Math.floor(Math.random() * friends.length)]; 18 | return ( 19 | {randomFriend} 20 | ); 21 | } 22 | } 23 | 24 | export default FriendThumb; 25 | -------------------------------------------------------------------------------- /src/common/hero/Hero.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import image from "../../images/hero/hero-main.png"; 4 | import EnhancedTable from "../table/EnhancedTable"; 5 | import "./hero.css"; 6 | interface HeroProps { 7 | showNetworkLatency: boolean; 8 | // toggleNetworkLatency: () => void; 9 | } 10 | interface HeroState { 11 | apiValue: []; 12 | } 13 | const Hero = (props: HeroProps) => { 14 | const [apiValue, setApiValue] = useState([]); 15 | useEffect(() => { 16 | const values = sessionStorage.getItem("responseTime") || "[]"; 17 | const jso = JSON.parse(values); 18 | setApiValue(jso); 19 | }, [props.showNetworkLatency]); 20 | 21 | return ( 22 | <> 23 | {props.showNetworkLatency ? ( 24 |
25 | 29 |
30 | ) : ( 31 | The more you read the more you know 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | export default Hero; 42 | -------------------------------------------------------------------------------- /src/common/hero/hero.css: -------------------------------------------------------------------------------- 1 | .full-width { 2 | width: 100%; 3 | height: auto; 4 | } 5 | 6 | .top-hero-padding { 7 | padding-top: 3rem; 8 | } -------------------------------------------------------------------------------- /src/common/starRating/StarRating.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Glyphicon } from 'react-bootstrap'; 3 | import "./starRating.css"; 4 | 5 | interface StarRatingProps { 6 | stars: number 7 | } 8 | 9 | class StarRating extends React.Component { 10 | render() { 11 | return ( 12 | 13 | = 1 ? "star" : "star-empty"} /> 14 | = 2 ? "star" : "star-empty"} /> 15 | = 3 ? "star" : "star-empty"} /> 16 | = 4 ? "star" : "star-empty"} /> 17 | = 5 ? "star" : "star-empty"} /> 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default StarRating; -------------------------------------------------------------------------------- /src/common/starRating/starRating.css: -------------------------------------------------------------------------------- 1 | .glyphicon-star { 2 | color: #FFD502; 3 | -webkit-text-fill-color: #FFD502; /* Will override color (regardless of order) */ 4 | -webkit-text-stroke-width: 1px; 5 | -webkit-text-stroke-color: #aaa; 6 | } 7 | 8 | .glyphicon-star-empty { 9 | color: #aaa; 10 | -webkit-text-fill-color: #aaa; /* Will override color (regardless of order) */ 11 | -webkit-text-stroke-width: 1px; 12 | -webkit-text-stroke-color: #aaa; 13 | } -------------------------------------------------------------------------------- /src/common/styles/common.css: -------------------------------------------------------------------------------- 1 | .btn-btn-black { 2 | background-color: #3b3b3b !important; 3 | border: 1px solid #000000 !important; 4 | display: inline-block; 5 | color: #ffffff !important; 6 | font-size: 13px; 7 | padding: 10px 19px; 8 | width: 120px; 9 | background-image: none !important; 10 | } 11 | 12 | .no-friends-padding { 13 | padding-top: 80px; 14 | } -------------------------------------------------------------------------------- /src/common/styles/gallery.css: -------------------------------------------------------------------------------- 1 | .thumbs { 2 | width: 8rem; 3 | height: auto; 4 | margin-left: 1.5rem; 5 | margin-right: 1.5rem; 6 | } 7 | 8 | .thumbnail { 9 | margin: 20px 0 0 0 !important; 10 | } 11 | 12 | .center { 13 | padding-top: 1rem; 14 | display: flex; 15 | justify-content: space-around; 16 | } 17 | 18 | .top-padding { 19 | padding-top: 1rem; 20 | } 21 | 22 | .ad-gallery { 23 | padding: 20px; 24 | -moz-box-shadow: inset 0 0 50px #f5f6f9; 25 | -webkit-box-shadow: inset 0 0 50px #f5f6f9; 26 | box-shadow: inset 0 0 50px #f5f6f9; 27 | } 28 | 29 | .padding-5 { 30 | padding: 5px !important; 31 | } 32 | 33 | .well-bs { 34 | background-color: #f5f6f9; 35 | padding: 20px; 36 | border-radius: 0px !important; 37 | } 38 | 39 | .well-bs h3 { 40 | color: #000; 41 | margin: 0 0 0px 0; 42 | font-weight: 400; 43 | } 44 | 45 | .well-bs a { 46 | color: #009dff !important; 47 | margin: 0 0 0px 0; 48 | font-weight: 400; 49 | font-size: 14px !important; 50 | } 51 | 52 | .container-category { 53 | padding: 20px; 54 | border: 1px #ddd solid; 55 | background-color: #fff; 56 | } 57 | 58 | .container-category h3 { 59 | margin: 0px; 60 | } 61 | 62 | .friend-thumb { 63 | border-radius: 50%; 64 | } 65 | 66 | .no-border { 67 | border: 0 !important; 68 | -moz-box-shadow: inset 0 0 0px #f5f6f9 !important; 69 | -webkit-box-shadow: inset 0 0 0px #f5f6f9 !important; 70 | box-shadow: inset 0 0 0px #f5f6f9 !important; 71 | } 72 | 73 | .no-padding-top { 74 | padding-top: 0px; 75 | } 76 | 77 | .no-margin-bottom { 78 | margin-bottom: 0px !important; 79 | } 80 | 81 | .no-padding-bottom { 82 | padding-bottom: 0px !important; 83 | } 84 | 85 | .padding-50 { 86 | padding: 80px !important; 87 | } 88 | 89 | .rating-container { 90 | padding: 0 11%; 91 | } 92 | 93 | .btn-black { 94 | background-color: #3b3b3b; 95 | border: 1px solid #000000; 96 | display: inline-block; 97 | cursor: pointer; 98 | color: #ffffff; 99 | font-size: 13px; 100 | padding: 10px 19px; 101 | text-decoration: none; 102 | width: 120px; 103 | border-radius: 0 !important; 104 | } 105 | 106 | .btn-black-center { 107 | display: block !important; 108 | margin-left: auto; 109 | margin-right: auto; 110 | } 111 | 112 | .btn-black:hover { 113 | color: #ffffff !important; 114 | } 115 | 116 | .btn-black:active { 117 | color: #ffffff !important; 118 | } 119 | 120 | .btn-black:focus { 121 | color: #ffffff !important; 122 | } 123 | 124 | .btn-transparent { 125 | color: blue; 126 | } 127 | 128 | a { 129 | font-size: 13px; 130 | } 131 | 132 | a:hover { 133 | text-decoration: none !important; 134 | } 135 | 136 | .loader { 137 | border: 2px solid #f3f3f3; /* Light grey */ 138 | border-top: 2px solid #2eadde; /* blue */ 139 | border-radius: 50%; 140 | width: 20px; 141 | height: 20px; 142 | animation: spin 2s linear infinite; 143 | position: absolute; 144 | left: 50%; 145 | z-index: 1; 146 | margin-top: 30px; 147 | } 148 | 149 | .loader-no-margin { 150 | border: 2px solid #f3f3f3; /* Light grey */ 151 | border-top: 2px solid #2eadde; /* blue */ 152 | border-radius: 50%; 153 | width: 20px; 154 | height: 20px; 155 | animation: spin 2s linear infinite; 156 | z-index: 1; 157 | } 158 | 159 | .padding-bottom-120 { 160 | padding-bottom: 120px; 161 | } 162 | 163 | @keyframes spin { 164 | 0% { 165 | transform: rotate(0deg); 166 | } 167 | 100% { 168 | transform: rotate(360deg); 169 | } 170 | } 171 | 172 | .order-date h4 { 173 | margin: 40px 0 0 2px; 174 | font-weight: 350; 175 | } 176 | 177 | .full-page { 178 | height: 100%; 179 | width: 100%; 180 | } 181 | 182 | .checkout-padding { 183 | padding-right: 21px; 184 | text-align: right; 185 | } 186 | 187 | .padding-total { 188 | padding: 10px 0; 189 | } 190 | 191 | .quantity-style { 192 | border: 1px solid #eee !important; 193 | background: transparent !important; 194 | border-radius: 4px; 195 | } 196 | 197 | .wrap-text { 198 | white-space: nowrap; 199 | overflow: hidden; 200 | text-overflow: clip; 201 | } 202 | -------------------------------------------------------------------------------- /src/common/styles/productRow.css: -------------------------------------------------------------------------------- 1 | .friend-images { 2 | width: 12rem; 3 | height: auto; 4 | } 5 | 6 | .product-thumb { 7 | height: 168px; 8 | margin-left: 2rem; 9 | margin-right: 2rem; 10 | } 11 | 12 | .friend-thumb { 13 | height: 5rem; 14 | } 15 | 16 | .white-box { 17 | display: block; 18 | padding: 4px; 19 | margin: 20px 0; 20 | background-color: #fff; 21 | border: 1px solid #ddd; 22 | border-radius: 0px; 23 | padding: 20px; 24 | } 25 | 26 | .margin-1 { 27 | margin: 1rem; 28 | } 29 | 30 | .no-margin { 31 | margin: 0; 32 | } 33 | 34 | .no-padding { 35 | padding: 0 !important; 36 | line-height: 0 !important; 37 | } 38 | 39 | .product-padding { 40 | padding: 20px !important; 41 | } 42 | 43 | .product-price h4 { 44 | font-weight: 300 !important; 45 | } 46 | .product-quantity-input-style { 47 | height: 40px; 48 | } 49 | -------------------------------------------------------------------------------- /src/common/table/EnhancedTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import clsx from "clsx"; 3 | import { 4 | createStyles, 5 | lighten, 6 | makeStyles, 7 | Theme, 8 | } from "@material-ui/core/styles"; 9 | import Table from "@material-ui/core/Table"; 10 | import TableBody from "@material-ui/core/TableBody"; 11 | import TableCell from "@material-ui/core/TableCell"; 12 | import TableContainer from "@material-ui/core/TableContainer"; 13 | import TableHead from "@material-ui/core/TableHead"; 14 | import TablePagination from "@material-ui/core/TablePagination"; 15 | import TableRow from "@material-ui/core/TableRow"; 16 | import TableSortLabel from "@material-ui/core/TableSortLabel"; 17 | import Toolbar from "@material-ui/core/Toolbar"; 18 | import Typography from "@material-ui/core/Typography"; 19 | import Paper from "@material-ui/core/Paper"; 20 | import { Grid, Button, CircularProgress } from "@material-ui/core"; 21 | // import SearchBar from "material-ui-search-bar"; 22 | const useStyles = makeStyles((theme: Theme) => 23 | createStyles({ 24 | root: { 25 | width: "100%", 26 | paddingRight: "10px", 27 | paddingLeft: "10px", 28 | }, 29 | gridStyle: { 30 | padding: "10px", 31 | }, 32 | button: { 33 | margin: "1px", 34 | }, 35 | paper: { 36 | width: "100%", 37 | marginBottom: theme.spacing(2), 38 | }, 39 | table: { 40 | minWidth: 750, 41 | }, 42 | visuallyHidden: { 43 | border: 0, 44 | clip: "rect(0 0 0 0)", 45 | height: 1, 46 | margin: -1, 47 | overflow: "hidden", 48 | padding: 0, 49 | position: "absolute", 50 | top: 20, 51 | width: 1, 52 | }, 53 | }) 54 | ); 55 | 56 | export interface Data { 57 | Name: string; 58 | URL: string; 59 | Path: string; 60 | Status: string; 61 | Time: string; 62 | Method: string; 63 | } 64 | 65 | interface HeadCell { 66 | disablePadding: boolean; 67 | id: keyof Data; 68 | label: string; 69 | numeric: boolean; 70 | } 71 | 72 | const headCells: HeadCell[] = [ 73 | { id: "Path", numeric: false, disablePadding: false, label: "Path" }, 74 | { id: "Status", numeric: false, disablePadding: false, label: "Status" }, 75 | { id: "Method", numeric: false, disablePadding: false, label: "Method" }, 76 | { id: "URL", numeric: false, disablePadding: false, label: "Size" }, 77 | { 78 | id: "Time", 79 | numeric: false, 80 | disablePadding: false, 81 | label: "Time", 82 | }, 83 | ]; 84 | 85 | interface EnhancedTableProps { 86 | classes: ReturnType; 87 | } 88 | 89 | function EnhancedTableHead(props: EnhancedTableProps) { 90 | const { classes } = props; 91 | const createSortHandler = (property: keyof Data) => ( 92 | event: React.MouseEvent 93 | ) => { 94 | // onRequestSort(event, property); 95 | }; 96 | 97 | return ( 98 | 99 | 100 | 101 | {headCells.map((headCell, index) => ( 102 | 109 | {headCell.label} 110 | 111 | ))} 112 | 113 | 114 | ); 115 | } 116 | 117 | const useToolbarStyles = makeStyles((theme: Theme) => 118 | createStyles({ 119 | root: { 120 | paddingLeft: theme.spacing(8), 121 | paddingRight: theme.spacing(4), 122 | }, 123 | highlight: 124 | theme.palette.type === "light" 125 | ? { 126 | color: theme.palette.secondary.main, 127 | backgroundColor: lighten(theme.palette.secondary.light, 0.85), 128 | } 129 | : { 130 | color: theme.palette.text.primary, 131 | backgroundColor: theme.palette.secondary.dark, 132 | }, 133 | title: { 134 | flex: "1 1 100%", 135 | }, 136 | }) 137 | ); 138 | 139 | const EnhancedTableToolbar = (props: { tableHeading: string }) => { 140 | const classes = useToolbarStyles(); 141 | 142 | return ( 143 | 144 | 151 | {props.tableHeading} 152 | 153 | 154 | ); 155 | }; 156 | type EnhancedTableDataProps = { 157 | networkapis: Data[]; 158 | tableHeading: string; 159 | }; 160 | export default function EnhancedTable(props: EnhancedTableDataProps) { 161 | const classes = useStyles(); 162 | // const [order, setOrder] = useState("desc"); 163 | const { networkapis } = props; 164 | // const [orderBy, setOrderBy] = useState("Path"); 165 | const [page, setPage] = useState(0); 166 | const [rowsPerPage, setRowsPerPage] = useState(10); 167 | 168 | const handleChangePage = (event: unknown, newPage: number) => { 169 | setPage(newPage); 170 | }; 171 | 172 | const handleChangeRowsPerPage = ( 173 | event: React.ChangeEvent 174 | ) => { 175 | setRowsPerPage(parseInt(event.target.value, 10)); 176 | }; 177 | 178 | return ( 179 |
180 | 181 | 182 | {networkapis && networkapis.length > 0 ? ( 183 |
184 | 185 | 192 | 193 | 194 | {networkapis 195 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 196 | .map((row, index) => { 197 | return ( 198 | 199 | 200 | 205 | {row.Path} 206 | 207 | 212 | {row.Status} 213 | 214 | 219 | {row.Method} 220 | 221 | 226 | {row.URL} 227 | 228 | 236 | {row.Time} 237 | 238 | 239 | ); 240 | })} 241 | 242 |
243 | 251 | 260 | 261 |
262 |
263 | ) : ( 264 |

No Data to Display

265 | )} 266 |
267 |
268 | ); 269 | } 270 | -------------------------------------------------------------------------------- /src/images/avatars/Brenda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/Brenda.png -------------------------------------------------------------------------------- /src/images/avatars/Erin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/Erin.png -------------------------------------------------------------------------------- /src/images/avatars/Jacob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/Jacob.png -------------------------------------------------------------------------------- /src/images/avatars/Jeff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/Jeff.png -------------------------------------------------------------------------------- /src/images/avatars/Jennifer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/Jennifer.png -------------------------------------------------------------------------------- /src/images/avatars/John.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/John.png -------------------------------------------------------------------------------- /src/images/avatars/Sarah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/avatars/Sarah.png -------------------------------------------------------------------------------- /src/images/bestSellers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers.png -------------------------------------------------------------------------------- /src/images/bestSellers/burgers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers/burgers.png -------------------------------------------------------------------------------- /src/images/bestSellers/italian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers/italian.png -------------------------------------------------------------------------------- /src/images/bestSellers/noodles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers/noodles.png -------------------------------------------------------------------------------- /src/images/bestSellers/pancakes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers/pancakes.png -------------------------------------------------------------------------------- /src/images/bestSellers/pineapple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers/pineapple.png -------------------------------------------------------------------------------- /src/images/bestSellers/umami.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bestSellers/umami.png -------------------------------------------------------------------------------- /src/images/bookstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/bookstore.png -------------------------------------------------------------------------------- /src/images/hero/hero-cars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-cars.png -------------------------------------------------------------------------------- /src/images/hero/hero-cookbooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-cookbooks.png -------------------------------------------------------------------------------- /src/images/hero/hero-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-database.png -------------------------------------------------------------------------------- /src/images/hero/hero-fairytales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-fairytales.png -------------------------------------------------------------------------------- /src/images/hero/hero-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-home.png -------------------------------------------------------------------------------- /src/images/hero/hero-main-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-main-old.png -------------------------------------------------------------------------------- /src/images/hero/hero-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-main.png -------------------------------------------------------------------------------- /src/images/hero/hero-science.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-science.png -------------------------------------------------------------------------------- /src/images/hero/hero-woodwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/hero/hero-woodwork.png -------------------------------------------------------------------------------- /src/images/pastOrders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/pastOrders.png -------------------------------------------------------------------------------- /src/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/screenshot.png -------------------------------------------------------------------------------- /src/images/supportedCards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/supportedCards.png -------------------------------------------------------------------------------- /src/images/yourpastorders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/yourpastorders.png -------------------------------------------------------------------------------- /src/images/yourshoppingcart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Macrometacorp/tutorial-cloudflare-bookstore/1518d385c8ed035033cbf2f21109381fe609b7d0/src/images/yourshoppingcart.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import './index.css'; 5 | import App from './App'; 6 | import registerServiceWorker from './registerServiceWorker'; 7 | 8 | import 'bootstrap/dist/css/bootstrap.css'; 9 | import 'bootstrap/dist/css/bootstrap-theme.css'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | registerServiceWorker(); 18 | -------------------------------------------------------------------------------- /src/modules/bestSellers/BestSellerProductRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { API } from "../../apiCalls"; 3 | 4 | import AddToCart from "../../common/AddToCart"; 5 | import FriendRecommendations from "../../common/friendRecommendations/FriendRecommendations"; 6 | import StarRating from "../../common/starRating/StarRating"; 7 | import "../../common/styles/productRow.css"; 8 | 9 | interface ProductRowProps { 10 | bookId: string; 11 | book: Book; 12 | } 13 | 14 | export interface Book { 15 | _key: string; 16 | price: number; 17 | category: string; 18 | name: string; 19 | rating: number; 20 | author: string; 21 | } 22 | 23 | interface ProductRowState { 24 | book: Book | undefined; 25 | } 26 | 27 | export class ProductRow extends React.Component< 28 | ProductRowProps, 29 | ProductRowState 30 | > { 31 | constructor(props: ProductRowProps) { 32 | super(props); 33 | 34 | this.state = { 35 | book: undefined, 36 | }; 37 | } 38 | 39 | async componentDidMount() { 40 | try { 41 | // const book = await this.getBook(); 42 | this.setState({ book: this.props.book }); 43 | } catch (e) { 44 | console.error(e); 45 | } 46 | } 47 | 48 | getBook() { 49 | return API.get("books", `/books/${this.props.bookId}`, null); 50 | } 51 | 52 | render() { 53 | if (!this.state.book) return null; 54 | 55 | return ( 56 |
57 |
58 |
59 | {`${this.state.book.name} 64 |
65 |
66 |

67 | {this.state.book.name} 68 | 69 |

${this.state.book.price}

70 |
71 |

72 |

73 | {this.state.book.category} 74 |

75 | {/*ABHISHEK*/} 76 | {/* */} 77 |
78 | 83 | Rating 84 | 85 | 86 | 90 | 91 |
92 | 93 |
94 |
95 |
96 | ); 97 | } 98 | } 99 | 100 | export default ProductRow; 101 | -------------------------------------------------------------------------------- /src/modules/bestSellers/BestSellers.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { API } from "../../apiCalls"; 3 | 4 | import BestSellerProductRow from "./BestSellerProductRow"; 5 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 6 | import { SearchBar } from "../search/searchBar/SearchBar"; 7 | 8 | interface BestSellersProps {} 9 | 10 | interface BestSellersState { 11 | isLoading: boolean; 12 | // books: { bookId: any }[]; 13 | books: { 14 | _key: string; 15 | author: string; 16 | name: string; 17 | price: number; 18 | rating: number; 19 | category: string; 20 | }[]; 21 | } 22 | 23 | export default class BestSellers extends React.Component< 24 | BestSellersProps, 25 | BestSellersState 26 | > { 27 | constructor(props: BestSellersProps) { 28 | super(props); 29 | 30 | this.state = { 31 | isLoading: true, 32 | books: [], 33 | }; 34 | } 35 | 36 | async componentDidMount() { 37 | try { 38 | const books = []; 39 | const bestSellers = await API.get("bestsellers", "/bestsellers", null); 40 | 41 | // Map the elasticache results to a book object 42 | for (var i = 0; i < bestSellers.length; i++) { 43 | // const bookId = bestSellers[i]; 44 | // books.push({ bookId }); 45 | const book = bestSellers[i]; 46 | books.push(book); 47 | } 48 | this.setState({ 49 | books: books, 50 | isLoading: false, 51 | }); 52 | } catch (error) { 53 | console.error(error); 54 | } 55 | } 56 | 57 | render() { 58 | return ( 59 |
60 | 61 |
62 |
63 |
64 |

Top 20 best sellers

65 |
66 | {this.state.isLoading ? ( 67 |
68 | ) : ( 69 | this.state.books 70 | .slice(0, 20) 71 | .map((book) => ( 72 | 77 | )) 78 | )} 79 |
80 |
81 |
82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/bestSellers/bestSellersBar/BestSellersBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LinkContainer } from "react-router-bootstrap"; 3 | import { NavItem } from "react-bootstrap"; 4 | import "../../../common/styles/gallery.css"; 5 | 6 | import burgers from "../../../images/bestSellers/burgers.png"; 7 | import italian from "../../../images/bestSellers/italian.png"; 8 | import noodles from "../../../images/bestSellers/noodles.png"; 9 | import pancakes from "../../../images/bestSellers/pancakes.png"; 10 | import pineapple from "../../../images/bestSellers/pineapple.png"; 11 | import umami from "../../../images/bestSellers/umami.png"; 12 | 13 | const bestSellers = [burgers, italian, noodles, pancakes, pineapple, umami]; 14 | 15 | export class BestSellersBar extends React.Component { 16 | render() { 17 | return ( 18 |
19 |
20 | 21 |

Bookstore
Best Sellers

22 |
23 |
24 |
25 | {bestSellers.map(book => 26 |
27 | 28 | 29 | 30 |
)} 31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | export default BestSellersBar; -------------------------------------------------------------------------------- /src/modules/cart/CartProductRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/productRow.css"; 3 | import { API } from "../../apiCalls"; 4 | import StarRating from "../../common/starRating/StarRating"; 5 | import FriendRecommendations from "../../common/friendRecommendations/FriendRecommendations"; 6 | import { Glyphicon } from "react-bootstrap"; 7 | import { Book } from "../bestSellers/BestSellerProductRow"; 8 | 9 | export interface Order { 10 | bookId: string; 11 | quantity: number; 12 | price: number; 13 | } 14 | 15 | interface CartProductRowProps { 16 | order: Order | any; 17 | book: any; 18 | calculateTotal: () => void; 19 | } 20 | 21 | interface CartProductRowState { 22 | book: Book | undefined; 23 | removeLoading: boolean; 24 | } 25 | 26 | export class CartProductRow extends React.Component< 27 | CartProductRowProps, 28 | CartProductRowState 29 | > { 30 | constructor(props: CartProductRowProps) { 31 | super(props); 32 | 33 | this.state = { 34 | book: undefined, 35 | removeLoading: false, 36 | }; 37 | } 38 | 39 | async componentDidMount() { 40 | try { 41 | // const book = this.getBook(this.props.order); 42 | const book = this.props.book; 43 | this.setState({ book }); 44 | } catch (e) { 45 | console.error(e); 46 | } 47 | } 48 | 49 | // getBook(order: any) { 50 | // return API.get("books", `/books/${order.bookId}`, null); 51 | // } 52 | 53 | onRemove = async () => { 54 | this.setState({ removeLoading: true }); 55 | await API.del("cart", "/cart", { 56 | body: { 57 | bookId: this.props.order.bookId, 58 | }, 59 | }); 60 | 61 | this.props.calculateTotal(); 62 | }; 63 | 64 | onQuantityUpdated = async (event: any) => { 65 | await API.put("cart", "/cart", { 66 | body: { 67 | bookId: this.props.order.bookId, 68 | quantity: parseInt(event.target.value, 10), 69 | }, 70 | }); 71 | }; 72 | 73 | render() { 74 | if (!this.state.book) return null; 75 | 76 | return ( 77 |
78 |
79 |
80 | {`${this.state.book.name} 85 |
86 |
87 |

88 | {this.state.book.name} 89 |
90 | ${this.state.book.price} 91 |
92 |

93 |

94 | {this.state.book.category} 95 |

96 | {/* */} 97 |
98 | Rating 99 |
100 |
101 | 109 | 110 | 121 | 122 |
123 |
124 |
125 |

126 | 127 |

128 |
129 |
130 |
131 | ); 132 | } 133 | } 134 | 135 | export default CartProductRow; 136 | -------------------------------------------------------------------------------- /src/modules/cart/ShoppingCart.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 3 | import { SearchBar } from "../search/searchBar/SearchBar"; 4 | import "../../common/hero/hero.css"; 5 | import { CartProductRow, Order } from "./CartProductRow"; 6 | import "../../common/styles/common.css"; 7 | import { API } from "../../apiCalls"; 8 | import { Redirect } from "react-router"; 9 | 10 | interface ShoppingCartProps {} 11 | 12 | interface ShoppingCartState { 13 | isLoading: boolean; 14 | orders: any[]; // FIXME 15 | orderTotal: number | undefined; 16 | toCheckout: boolean; 17 | } 18 | 19 | export default class ShoppingCart extends Component< 20 | ShoppingCartProps, 21 | ShoppingCartState 22 | > { 23 | constructor(props: ShoppingCartProps) { 24 | super(props); 25 | 26 | this.state = { 27 | isLoading: true, 28 | orders: [], 29 | orderTotal: undefined, 30 | toCheckout: false, 31 | }; 32 | } 33 | 34 | async componentDidMount() { 35 | try { 36 | const ordersInCart = await this.listOrdersInCart(); 37 | this.setState({ 38 | orders: ordersInCart, 39 | }); 40 | } catch (e) { 41 | console.error(e); 42 | } 43 | 44 | this.getOrderTotal(); 45 | this.setState({ isLoading: false }); 46 | } 47 | 48 | listOrdersInCart() { 49 | return API.get("cart", "/cart", null); 50 | } 51 | 52 | getOrderTotal = async (shouldMakeCall: boolean = false) => { 53 | let ordersInCart = this.state.orders; 54 | 55 | if (shouldMakeCall) { 56 | ordersInCart = await this.listOrdersInCart(); 57 | this.setState({ 58 | orders: ordersInCart, 59 | }); 60 | } 61 | 62 | let total = ordersInCart 63 | .reduce((total: number, orderObj: { order: Order }) => { 64 | const { order } = orderObj; 65 | return total + order.price * order.quantity; 66 | }, 0) 67 | .toFixed(2); 68 | 69 | this.setState({ 70 | orderTotal: total, 71 | }); 72 | }; 73 | 74 | onCheckout = () => { 75 | this.setState({ 76 | toCheckout: true, 77 | }); 78 | }; 79 | 80 | render() { 81 | if (this.state.toCheckout) return ; 82 | 83 | return this.state.isLoading ? ( 84 |
85 | ) : ( 86 |
87 | 88 |
89 |
90 |

Shopping cart

91 |
92 | {this.state.orders.map((order) => ( 93 | this.getOrderTotal(true)} 98 | /> 99 | ))} 100 |
101 | 109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/modules/category/CategoryGallery.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/gallery.css"; 3 | import { API } from "../../apiCalls"; 4 | import CategoryGalleryBook from "./CategoryGalleryBook"; 5 | import { Book } from "../bestSellers/BestSellerProductRow"; 6 | 7 | interface CategoryGalleryProps { 8 | match: any; 9 | } 10 | 11 | interface CategoryGalleryState { 12 | isLoading: boolean; 13 | books: Book[]; 14 | } 15 | 16 | export class CategoryGallery extends React.Component { 17 | constructor(props: CategoryGalleryProps) { 18 | super(props); 19 | 20 | this.state = { 21 | isLoading: true, 22 | books: [] 23 | }; 24 | } 25 | 26 | async componentDidMount() { 27 | try { 28 | const books = await this.listBooks(); 29 | this.setState({ books }); 30 | } catch (e) { 31 | console.error(e); 32 | } 33 | 34 | this.setState({ isLoading: false }); 35 | } 36 | 37 | listBooks() { 38 | return API.get("books", `/books?category=${this.props.match.params.id}`, null); 39 | } 40 | 41 | render() { 42 | return ( 43 | this.state.isLoading ?
: 44 |
45 |
46 |
47 |

{this.props.match.params.id}

48 |
49 | {this.state.books.map(book => )} 50 |
51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | export default CategoryGallery; -------------------------------------------------------------------------------- /src/modules/category/CategoryGalleryBook.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/gallery.css"; 3 | import StarRating from "../../common/starRating/StarRating"; 4 | import AddToCart from "../../common/AddToCart"; 5 | import { Book } from "../bestSellers/BestSellerProductRow"; 6 | 7 | interface CategoryGalleryBookProps { 8 | book: Book; 9 | } 10 | 11 | export class CategoryGalleryBook extends React.Component< 12 | CategoryGalleryBookProps 13 | > { 14 | render() { 15 | if (!this.props.book) return; 16 | return ( 17 |
18 |
19 |

20 | 21 | {`$${this.props.book.price}`} 22 |

23 | {`${this.props.book.name} 28 |
29 |

{this.props.book.name}

30 | 35 |
36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default CategoryGalleryBook; 43 | -------------------------------------------------------------------------------- /src/modules/category/CategoryGalleryTeaser.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/gallery.css"; 3 | import { LinkContainer } from "react-router-bootstrap"; 4 | import { API } from "../../apiCalls"; 5 | import CategoryGalleryBook from "./CategoryGalleryBook"; 6 | import { Book } from "../bestSellers/BestSellerProductRow"; 7 | 8 | interface CategoryGalleryTeaserProps {} 9 | 10 | interface CategoryGalleryTeaserState { 11 | isLoading: boolean; 12 | // ABHISHEK: correct type 13 | books: Book[] | any; 14 | } 15 | 16 | export class CategoryGalleryTeaser extends React.Component< 17 | CategoryGalleryTeaserProps, 18 | CategoryGalleryTeaserState 19 | > { 20 | constructor(props: CategoryGalleryTeaserProps) { 21 | super(props); 22 | 23 | this.state = { 24 | isLoading: true, 25 | books: [], 26 | }; 27 | } 28 | 29 | async componentDidMount() { 30 | try { 31 | const books = await this.listBooks(); 32 | this.setState({ books }); 33 | } catch (e) { 34 | console.error(e); 35 | } 36 | 37 | this.setState({ isLoading: false }); 38 | } 39 | 40 | listBooks() { 41 | return API.get("books", "/books?category=Cookbooks", null); 42 | } 43 | 44 | render() { 45 | return this.state.isLoading ? ( 46 |
47 | ) : ( 48 |
49 |
50 |
51 |

52 | Cookbooks{" "} 53 | 54 | 55 | Browse cookbooks 56 | 57 | 58 |

59 |
60 | {this.state.books.slice(0, 4).map((book: any) => ( 61 | 62 | ))} 63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | } 70 | 71 | export default CategoryGalleryTeaser; 72 | -------------------------------------------------------------------------------- /src/modules/category/CategoryView.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useEffect, useState } from "react"; 2 | import { CategoryNavBar } from "./categoryNavBar/CategoryNavBar"; 3 | import { SearchBar } from "../search/searchBar/SearchBar"; 4 | import { BestSellersBar } from "../bestSellers/bestSellersBar/BestSellersBar"; 5 | import { CategoryGallery } from "./CategoryGallery"; 6 | 7 | import database from "../../images/hero/hero-database.png"; 8 | import cars from "../../images/hero/hero-cars.png"; 9 | import cooks from "../../images/hero/hero-cookbooks.png"; 10 | import fairy from "../../images/hero/hero-fairytales.png"; 11 | import home from "../../images/hero/hero-home.png"; 12 | import scifi from "../../images/hero/hero-science.png"; 13 | import woodwork from "../../images/hero/hero-woodwork.png"; 14 | 15 | import "../../common/hero/hero.css"; 16 | import { categories } from "./categoryNavBar/categories"; 17 | import EnhancedTable from "../../common/table/EnhancedTable"; 18 | 19 | interface CategoryViewProps { 20 | match: any; 21 | showNetworkLatency: boolean; 22 | } 23 | interface CategoryViewState { 24 | apiValue: []; 25 | } 26 | 27 | const CategoryView = (props: CategoryViewProps) => { 28 | const [apiValue, setApiValue] = useState([]); 29 | useEffect(() => { 30 | const values = sessionStorage.getItem("responseTime") || "[]"; 31 | const responseTimeValue = JSON.parse(values); 32 | setApiValue(responseTimeValue); 33 | }, [props.showNetworkLatency]); 34 | 35 | const getImage = () => { 36 | switch (props.match.params.id) { 37 | case categories.cooks: 38 | return cooks; 39 | case categories.database: 40 | return database; 41 | case categories.fairy: 42 | return fairy; 43 | case categories.scifi: 44 | return scifi; 45 | case categories.home: 46 | return home; 47 | case categories.cars: 48 | return cars; 49 | case categories.woodwork: 50 | return woodwork; 51 | default: 52 | return cooks; 53 | } 54 | }; 55 | return ( 56 |
57 | 58 | {props.showNetworkLatency ? ( 59 | 63 | ) : ( 64 | {`${getImage()} 69 | )} 70 | 71 | 72 |
73 | ); 74 | }; 75 | export default CategoryView; 76 | -------------------------------------------------------------------------------- /src/modules/category/categoryNavBar/CategoryNavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { categories } from "./categories"; 3 | import "./categories.css"; 4 | 5 | export class CategoryNavBar extends React.Component { 6 | render() { 7 | return ( 8 | 22 | ); 23 | } 24 | } 25 | 26 | export default CategoryNavBar; 27 | -------------------------------------------------------------------------------- /src/modules/category/categoryNavBar/categories.css: -------------------------------------------------------------------------------- 1 | .justify-content-space-between { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .category-link { 7 | color: inherit !important; 8 | } 9 | 10 | .nav-cat { 11 | font-weight: 200; 12 | font-size: 12px; 13 | background-color: #292929; 14 | color: #fff; 15 | } 16 | 17 | .top-hero-padding { 18 | padding-top: 0 !important; 19 | } 20 | .nav > .li > .a { 21 | padding: 15px; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/category/categoryNavBar/categories.ts: -------------------------------------------------------------------------------- 1 | export const categories = { 2 | cooks: "Cookbooks", 3 | database: "Database", 4 | fairy: "Fairy Tales", 5 | scifi: "Science Fiction", 6 | home: "Home Improvement", 7 | cars: "Cars", 8 | woodwork: "Woodwork", 9 | } -------------------------------------------------------------------------------- /src/modules/checkout/Checkout.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 3 | import { SearchBar } from "../search/searchBar/SearchBar"; 4 | import { CheckoutForm } from "./checkoutForm/CheckoutForm"; 5 | 6 | export default class Checkout extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 |
13 |

Checkout

14 |
15 |
16 | 17 |
18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /src/modules/checkout/CheckoutConfirm.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Redirect } from "react-router"; 3 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 4 | import { SearchBar } from "../search/searchBar/SearchBar"; 5 | 6 | import bestSellers from "../../images/bestSellers.png"; 7 | import pastOrders from "../../images/pastOrders.png"; 8 | 9 | import "./checkout.css"; 10 | 11 | interface CheckoutProps { } 12 | 13 | interface CheckoutState { 14 | toPastOrders: boolean; 15 | } 16 | 17 | export default class Checkout extends Component { 18 | constructor(props: CheckoutProps) { 19 | super(props); 20 | 21 | this.state = { 22 | toPastOrders: false, 23 | }; 24 | } 25 | 26 | onViewReceipt = () => { 27 | this.setState({ 28 | toPastOrders: true 29 | }); 30 | } 31 | 32 | render() { 33 | if (this.state.toPastOrders) return 34 | 35 | return ( 36 |
37 | 38 |
39 |
40 |

Purchase confirmed

41 |
42 |
43 |
44 |
45 |

Your purchase is complete!

46 | 47 |
48 |
49 | Best sellers 50 | Past orders 51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/checkout/checkout.css: -------------------------------------------------------------------------------- 1 | .checkout-img { 2 | float: left; 3 | width: 50%; 4 | padding: 20px 5 | } 6 | 7 | .padding-50 { 8 | padding: 50px; 9 | } -------------------------------------------------------------------------------- /src/modules/checkout/checkoutForm/CheckoutForm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormGroup, FormControl, ControlLabel, Form, FormControlProps } from "react-bootstrap"; 3 | 4 | import "./checkoutForm.css"; 5 | import supportedCards from "../../../images/supportedCards.png"; 6 | import { API } from "../../../apiCalls"; 7 | import { Redirect } from "react-router"; 8 | 9 | interface CheckoutFormProps {} 10 | 11 | interface CheckoutFormState { 12 | card: string; 13 | expDate: string | undefined; 14 | ccv: string; 15 | isLoading: boolean; 16 | toCart: boolean; 17 | orders: any[]; 18 | toConfirm: boolean; 19 | } 20 | 21 | export class CheckoutForm extends React.Component { 22 | constructor(props: CheckoutFormProps) { 23 | super(props); 24 | 25 | this.state = { 26 | card: '1010101010101010', 27 | expDate: undefined, 28 | ccv: '123', 29 | isLoading: true, 30 | toCart: false, 31 | orders: [], 32 | toConfirm: false, 33 | }; 34 | } 35 | 36 | async componentDidMount() { 37 | try { 38 | let orders = await this.listOrdersInCart(); 39 | orders = orders.map((orderObj: any) => orderObj.order); 40 | this.setState({ 41 | orders: orders 42 | }); 43 | } catch (e) { 44 | console.error(e); 45 | } 46 | 47 | this.setState({ isLoading: false }); 48 | } 49 | 50 | listOrdersInCart() { 51 | return API.get("cart", "/cart", null); 52 | } 53 | 54 | getOrderTotal = () => { 55 | return this.state.orders.reduce((total, book) => { 56 | return total + book.price * book.quantity 57 | }, 0).toFixed(2); 58 | } 59 | 60 | getCardNumberValidationState() { 61 | const length = this.state.card.length; 62 | if (length >= 15 && length <= 19) return 'success'; 63 | else if (length !== 0 && (length < 15 || length > 19)) return 'error'; 64 | return null; 65 | } 66 | 67 | handleChange = (event: React.FormEvent) => { 68 | const target = event.target as HTMLInputElement 69 | this.setState({ 70 | ...this.state, 71 | [target.name as any]: target.value 72 | }); 73 | } 74 | 75 | onCheckout = () => { 76 | const orders = this.state.orders; 77 | API.post("orders", "/orders", { 78 | body: { 79 | books: orders 80 | } 81 | }).then(() => this.setState({ 82 | toConfirm: true 83 | })); 84 | } 85 | 86 | render() { 87 | if (this.state.toConfirm) return 88 | 89 | if (this.state.isLoading) return null; 90 | return ( 91 |
92 |
93 |
94 | Supported cards 95 |
96 | 99 | Card number 100 | 105 | 106 | 107 |
108 | 110 | Expiration date 111 | 116 | 117 | 118 | 121 | CCV 122 | 127 | 128 | 129 |
130 |
131 |
132 |
133 |
134 | 135 |
136 |
137 | ); 138 | } 139 | } 140 | 141 | export default CheckoutForm; 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/modules/checkout/checkoutForm/checkoutForm.css: -------------------------------------------------------------------------------- 1 | .checkout { 2 | width: 400px; 3 | } 4 | 5 | .form-row { 6 | display: flex; 7 | flex-direction: row; 8 | justify-content: space-between; 9 | } 10 | 11 | .ccv { 12 | width: 150px; 13 | } -------------------------------------------------------------------------------- /src/modules/friends/FriendsBought.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ProductRow } from "./ProductRow"; 3 | import { API } from "../../apiCalls"; 4 | 5 | interface FriendsBoughtProps { 6 | 7 | } 8 | 9 | interface FriendsBoughtState { 10 | isLoading: boolean; 11 | recommendations: any[]; // FIXME 12 | } 13 | 14 | export class FriendsBought extends React.Component { 15 | constructor(props: FriendsBoughtProps) { 16 | super(props); 17 | 18 | this.state = { 19 | isLoading: true, 20 | recommendations: [] 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | API.get("recommendations", "/recommendations", null) 26 | .then(response => { 27 | this.setState({ 28 | recommendations: response, 29 | isLoading: false 30 | }); 31 | }) 32 | .catch(error => console.error(error)); 33 | } 34 | 35 | render() { 36 | if (this.state.isLoading) return null; 37 | 38 | return ( 39 |
40 |
41 |

Books your friends have bought

42 |
43 | {this.state.recommendations.slice(0,5).map(recommendation => 44 | 45 | )} 46 |
47 | ); 48 | } 49 | } 50 | 51 | export default FriendsBought; -------------------------------------------------------------------------------- /src/modules/friends/ProductRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/productRow.css"; 3 | import StarRating from "../../common/starRating/StarRating"; 4 | import { API } from "../../apiCalls"; 5 | import AddToCart from "../../common/AddToCart"; 6 | import FriendRecommendations from "../../common/friendRecommendations/FriendRecommendations"; 7 | import { Book } from "../bestSellers/BestSellerProductRow"; 8 | 9 | interface ProductRowProps { 10 | book: Book; 11 | bookId: string; 12 | } 13 | 14 | interface ProductRowState { 15 | book: Book | undefined; 16 | } 17 | 18 | export class ProductRow extends React.Component< 19 | ProductRowProps, 20 | ProductRowState 21 | > { 22 | constructor(props: ProductRowProps) { 23 | super(props); 24 | 25 | this.state = { 26 | book: undefined, 27 | }; 28 | } 29 | 30 | componentDidMount() { 31 | this.setState({ book: this.props.book }); 32 | // API.get("books", `/books/${this.props.bookId}`, null) 33 | // .then((response) => this.setState({ book: response })) 34 | // .catch((error) => console.error(error)); 35 | } 36 | 37 | render() { 38 | if (!this.state.book) return null; 39 | 40 | return ( 41 |
42 |
43 |
44 | {`${this.state.book.name} 49 |
50 |
51 |

52 | {this.state.book.name} 53 | ${this.state.book.price} 54 |

55 |

56 | {this.state.book.category} 57 |

58 | 59 |
60 | Rating 61 | 62 | 66 | 67 |
68 | 69 |
70 |
71 |
72 | ); 73 | } 74 | } 75 | 76 | export default ProductRow; 77 | -------------------------------------------------------------------------------- /src/modules/notFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./notFound.css"; 3 | 4 | export default () => 5 |
6 |

Sorry, page not found!

7 |
; -------------------------------------------------------------------------------- /src/modules/notFound/notFound.css: -------------------------------------------------------------------------------- 1 | .not-found { 2 | padding-top: 100px; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /src/modules/pastPurchases/PastPurchases.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 3 | import { SearchBar } from "../search/searchBar/SearchBar"; 4 | import "../../common/hero/hero.css"; 5 | import { PurchasedProductRow } from "./PurchasedProductRow"; 6 | import { Auth, API } from "../../apiCalls"; 7 | import bestSellers from "../../images/bestSellers.png"; 8 | import yourshoppingcart from "../../images/yourshoppingcart.png"; 9 | import { Order } from "../cart/CartProductRow"; 10 | 11 | interface PastPurchasesProps {} 12 | 13 | interface Purchases { 14 | orderDate: number; 15 | _key: string; 16 | books: Order[]; 17 | } 18 | 19 | interface PastPurchasesState { 20 | userInfo: any; // FIXME 21 | isLoading: boolean; 22 | orders: Purchases[]; 23 | } 24 | 25 | export default class PastPurchases extends Component< 26 | PastPurchasesProps, 27 | PastPurchasesState 28 | > { 29 | constructor(props: PastPurchasesProps) { 30 | super(props); 31 | 32 | this.state = { 33 | userInfo: null, 34 | isLoading: true, 35 | orders: [], 36 | }; 37 | } 38 | 39 | async componentDidMount() { 40 | const userInfo = await Auth.currentUserInfo(); 41 | this.setState({ userInfo }); 42 | 43 | try { 44 | const orders = await this.listOrders(); 45 | this.setState({ 46 | orders: orders, 47 | isLoading: false, 48 | }); 49 | } catch (e) { 50 | console.error(e); 51 | } 52 | } 53 | 54 | listOrders() { 55 | return API.get("orders", "/orders", null); 56 | } 57 | 58 | getPrettyDate = (orderDate: number) => { 59 | const date = new Date(orderDate); 60 | return `${ 61 | date.getMonth() + 1 62 | }/${date.getDate()}/${date.getFullYear()} ${date.getHours()}:${ 63 | date.getMinutes() < 10 ? "0" : "" 64 | }${date.getMinutes()}`; 65 | }; 66 | 67 | render() { 68 | return ( 69 |
70 | 71 |
72 | {this.state.userInfo && ( 73 |
74 |

{`Hello ${this.state.userInfo.attributes.email}!`}

75 |
76 | )} 77 |
78 |

Past purchases

79 |
80 | {!this.state.isLoading && 81 | this.state.orders && 82 | this.state.orders.length>0 &&this.state.orders 83 | .sort((order1, order2) => order2.orderDate - order1.orderDate) 84 | .map((order) => ( 85 |
86 |

{`Order date: ${this.getPrettyDate( 87 | order.orderDate 88 | )}`}

89 | {order.books.map((book) => ( 90 | 91 | ))} 92 |
93 | ))} 94 | 95 | 111 |
112 |
113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/modules/pastPurchases/PurchasedProductRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/productRow.css"; 3 | import StarRating from "../../common/starRating/StarRating"; 4 | import { API } from "../../apiCalls"; 5 | import AddToCart from "../../common/AddToCart"; 6 | import FriendRecommendations from "../../common/friendRecommendations/FriendRecommendations"; 7 | import { Book } from "../bestSellers/BestSellerProductRow"; 8 | import { Order } from "../cart/CartProductRow"; 9 | 10 | interface PurchasedProductRowProps { 11 | order: Order; 12 | } 13 | 14 | interface PurchasedProductRowState { 15 | book: any; 16 | } 17 | 18 | export class PurchasedProductRow extends React.Component { 19 | constructor(props: PurchasedProductRowProps) { 20 | super(props); 21 | 22 | this.state = { 23 | book: undefined 24 | }; 25 | } 26 | 27 | async componentDidMount() { 28 | try { 29 | // const book = await this.getBook(this.props.order); 30 | this.setState({ book: this.props.order }); 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | } 35 | 36 | // getBook(order: Order) { 37 | // return API.get("books", `/books/${order.bookId}`, null); 38 | // } 39 | 40 | render() { 41 | if (!this.state.book) { 42 | return ( 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | return ( 54 |
55 |
56 |
57 | {`${this.state.book.name} 58 |
59 |
60 |

{this.state.book.name} 61 |
62 | {`${this.props.order.quantity} @ ${this.state.book.price}`} 63 |
64 |

65 | {this.state.book.category} 66 | {/* */} 67 |
68 | Rating 69 | 70 |
71 | 72 |
73 |
74 |
75 | ); 76 | } 77 | } 78 | 79 | export default PurchasedProductRow; 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/modules/search/SearchGallery.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../../common/styles/gallery.css"; 3 | import { API } from "../../apiCalls"; 4 | import CategoryGalleryBook from "../category/CategoryGalleryBook"; 5 | import { Book } from "../bestSellers/BestSellerProductRow"; 6 | 7 | interface SearchGalleryProps { 8 | match: any; 9 | } 10 | 11 | interface SearchGalleryState { 12 | isLoading: boolean; 13 | books: Book[]; 14 | } 15 | 16 | export class SearchGallery extends React.Component { 17 | constructor(props: SearchGalleryProps) { 18 | super(props); 19 | 20 | this.state = { 21 | isLoading: true, 22 | books: [] 23 | }; 24 | } 25 | 26 | async componentDidMount() { 27 | try { 28 | const searchResults = await this.searchBooks(); 29 | 30 | // Map the search results to a book object 31 | // const books = []; 32 | // for (var i = 0; i < searchResults.hits.total; i++) { 33 | // var hit = searchResults.hits.hits[i] && searchResults.hits.hits[i]._source; 34 | // hit && books.push({ 35 | // _key: hit.id.$, 36 | // author: hit.author.S, 37 | // category: hit.category.S, 38 | // // id: hit.id.S, 39 | // name: hit.name.S, 40 | // price: hit.price.N, 41 | // rating: hit.rating.N, 42 | // }); 43 | // } 44 | 45 | this.setState({ 46 | books: searchResults 47 | }); 48 | } catch (e) { 49 | console.error(e); 50 | } 51 | 52 | this.setState({ isLoading: false }); 53 | } 54 | 55 | searchBooks() { 56 | return API.get("search", `/search?q=${this.props.match.params.id}`, null); 57 | } 58 | 59 | render() { 60 | return ( 61 | this.state.isLoading ?
: 62 |
63 |
64 |
65 |

Search results

66 |
67 | {this.state.books.map(book => )} 68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | } 75 | 76 | export default SearchGallery; -------------------------------------------------------------------------------- /src/modules/search/SearchView.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 3 | import { SearchBar } from "./searchBar/SearchBar"; 4 | import { SearchGallery } from "./SearchGallery"; 5 | 6 | interface SearchViewProps { 7 | match: any; 8 | } 9 | 10 | export default class SearchView extends Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /src/modules/search/searchBar/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./searchBar.css"; 3 | import { Redirect } from "react-router"; 4 | 5 | interface SearchBarProps {} 6 | 7 | interface SearchBarState { 8 | redirect: string | undefined; 9 | value: string; 10 | } 11 | 12 | export class SearchBar extends React.Component { 13 | constructor(props: SearchBarProps) { 14 | super(props); 15 | 16 | this.state = { 17 | redirect: undefined, 18 | value: "", 19 | }; 20 | } 21 | 22 | handleChange = (event: React.ChangeEvent) => { 23 | const target = event.currentTarget as HTMLInputElement; 24 | this.setState({ value: target.value }); 25 | }; 26 | 27 | onSearch = () => { 28 | if (this.state.value.trim()) { 29 | this.setState({ 30 | redirect: `/search/${this.state.value}`, 31 | }); 32 | } 33 | }; 34 | 35 | render() { 36 | return ( 37 |
38 |
39 |
40 |
41 | 50 |
51 | 65 |
66 | 67 | {this.state.redirect && } 68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | } 75 | 76 | export default SearchBar; 77 | -------------------------------------------------------------------------------- /src/modules/search/searchBar/searchBar.css: -------------------------------------------------------------------------------- 1 | .search-padding { 2 | padding-left: 25px !important; 3 | padding-right: 1rem; 4 | } 5 | 6 | .title-padding { 7 | padding: 5px 0 5px 0px; 8 | margin: 0; 9 | text-align: center; 10 | } 11 | 12 | .no-radius { 13 | border-radius: 0 !important; 14 | } 15 | 16 | .no-margin { 17 | margin: 0 24px 0 0 !important; 18 | } 19 | 20 | .mainsearch { 21 | /* background-color: #4a4a4a; */ 22 | /* padding: 10px 0 10px 10px; */ 23 | } 24 | .search-styling { 25 | display: flex; 26 | flex-direction: row; 27 | margin-top: -1vh; 28 | } 29 | 30 | .btn-orange { 31 | background-color: #2eadde; 32 | } 33 | 34 | .no-margin-top { 35 | margin-top: 0px !important; 36 | } 37 | 38 | /*test color*/ 39 | 40 | .orange { 41 | color: #2eadde; 42 | } 43 | 44 | .white { 45 | color: #ffffff; 46 | } 47 | 48 | .addon-black { 49 | color: #ffffff !important; 50 | background-color: #000 !important; 51 | border: #000 !important; 52 | font-weight: 200 !important; 53 | } 54 | 55 | .nav > li > a:hover, 56 | .nav > li > a:focus { 57 | background-color: transparent !important; 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/signup/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import screenshot from "../../images/screenshot.png"; 3 | import yourpastorders from "../../images/yourpastorders.png"; 4 | import bestSellers from "../../images/bestSellers.png"; 5 | import yourshoppingcart from "../../images/yourshoppingcart.png"; 6 | import Hero from "../../common/hero/Hero"; 7 | import { CategoryNavBar } from "../category/categoryNavBar/CategoryNavBar"; 8 | import { SearchBar } from "../search/searchBar/SearchBar"; 9 | import { BestSellersBar } from "../bestSellers/bestSellersBar/BestSellersBar"; 10 | import { CategoryGalleryTeaser } from "../category/CategoryGalleryTeaser"; 11 | import { FriendsBought } from "../friends/FriendsBought"; 12 | import { LinkContainer } from "react-router-bootstrap"; 13 | import "./home.css"; 14 | import { Auth } from "../../apiCalls"; 15 | import { Button, Glyphicon } from "react-bootstrap"; 16 | 17 | interface HomeProps { 18 | isAuthenticated: boolean; 19 | userHasAuthenticated: (authenticated: boolean) => void; 20 | showNetworkLatency: boolean; 21 | } 22 | 23 | interface HomeState { 24 | isLoading: boolean; 25 | } 26 | 27 | export default class Home extends Component { 28 | constructor(props: HomeProps) { 29 | super(props); 30 | 31 | this.state = { 32 | isLoading: false, 33 | }; 34 | } 35 | 36 | async componentDidMount() { 37 | if (!this.props.isAuthenticated) { 38 | return; 39 | } 40 | 41 | this.setState({ isLoading: true }); 42 | } 43 | 44 | onLogin = async (event: any) => { 45 | event.preventDefault(); 46 | this.setState({ isLoading: true }); 47 | 48 | try { 49 | await Auth.signIn("guest@macrometa.io", "Abcd1234"); 50 | 51 | this.setState({ isLoading: false }, () => { 52 | this.props.userHasAuthenticated(true); 53 | }); 54 | // this.setState({ redirect: true }); 55 | } catch (e) { 56 | console.error(e.message); 57 | this.setState({ isLoading: false }); 58 | } 59 | }; 60 | renderLanding() { 61 | return ( 62 |
63 |
64 |

65 | Welcome to the Edge Commerce Bookstore example app built entirely with 66 | Cloudflare Workers & Marcometa Global Data Network. It's entirely 67 | serverless and geo-distributed, which provides a lovely developer 68 | experience when building it and unparalleled performance. 69 |

70 |

71 | Creating a new user account to access the app will give you the full 72 | 'shopping' experience. If you don't want to take the time to sign up, 73 | you can access a shared demo account with a single click below (more 74 | than one person may be logged into the shared account at once, so you 75 | might see some unexpected behavior). 76 |

77 |

78 | Learn more about the architecture of the app by checking out the 79 | source code in this{" "} 80 | 84 | github repository 85 | 86 | . 87 |

88 |
89 | 94 |
95 |

{ 97 | this.onLogin(event); 98 | }} 99 | className="link-click" 100 | style={{ 101 | color: "#2eadde", 102 | fontSize: "14px", 103 | fontWeight: "bold", 104 | }} 105 | > 106 | {this.state.isLoading && ( 107 | 108 | )} 109 | Log in to shared guest account 110 |

111 |
112 |
113 |
114 | Screenshot 119 | 120 |
121 |

122 | This example app is an exact replica of the Amazon Web Services{" "} 123 | 124 | Amazon Book Store example app 125 | {" "} 126 | using Cloudflare and Macrometa instead of AWS. 127 |

128 |
129 |
130 |
141 |
148 |
156 |

163 | Get started using Macrometa 164 |

165 |
166 |
173 |
178 | 184 |
185 |
186 |
187 |
188 |
189 | ); 190 | } 191 | 192 | renderHome() { 193 | return ( 194 |
195 | 196 | 197 | 198 |
199 |
200 |
201 | 202 | Past orders 203 | 204 |
205 |
206 |
207 |
208 | 209 | Shopping cart 210 | 211 |
212 |
213 |
214 |
215 | 216 | Best sellers 217 | 218 |
219 |
220 |
221 | 222 | 223 |
224 | ); 225 | } 226 | 227 | render() { 228 | return ( 229 |
230 | {this.props.isAuthenticated ? this.renderHome() : this.renderLanding()} 231 |
232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/modules/signup/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "react-router"; 3 | import { 4 | FormGroup, 5 | FormControl, 6 | ControlLabel, 7 | Button, 8 | Glyphicon, 9 | } from "react-bootstrap"; 10 | import { Auth } from "../../apiCalls"; 11 | import "./login.css"; 12 | 13 | const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 14 | 15 | interface LoginProps { 16 | isAuthenticated: boolean; 17 | userHasAuthenticated: (authenticated: boolean) => void; 18 | } 19 | 20 | interface LoginState { 21 | loading: boolean; 22 | redirect: boolean; 23 | email: string; 24 | password: string; 25 | emailValid: "success" | "error" | "warning" | undefined; 26 | passwordValid: "success" | "error" | "warning" | undefined; 27 | } 28 | 29 | export default class Login extends React.Component { 30 | constructor(props: LoginProps) { 31 | super(props); 32 | 33 | this.state = { 34 | loading: false, 35 | redirect: false, 36 | email: "", 37 | password: "", 38 | emailValid: undefined, 39 | passwordValid: undefined, 40 | }; 41 | } 42 | 43 | onEmailChange = (event: React.FormEvent) => { 44 | const target = event.target as HTMLInputElement; 45 | this.setState({ 46 | email: target.value, 47 | emailValid: emailRegex.test(target.value.toLowerCase()) 48 | ? "success" 49 | : "error", 50 | }); 51 | }; 52 | 53 | onPasswordChange = (event: React.FormEvent) => { 54 | const target = event.target as HTMLInputElement; 55 | this.setState({ 56 | password: target.value, 57 | passwordValid: target.value.length < 8 ? "error" : "success", 58 | }); 59 | }; 60 | 61 | onLogin = async (event: React.FormEvent) => { 62 | event.preventDefault(); 63 | this.setState({ loading: true }); 64 | 65 | try { 66 | await Auth.signIn(this.state.email, this.state.password); 67 | this.props.userHasAuthenticated(true); 68 | this.setState({ redirect: true }); 69 | } catch (e) { 70 | console.error(e.message); 71 | this.setState({ loading: false }); 72 | } 73 | }; 74 | 75 | render() { 76 | if (this.state.redirect) return ; 77 | 78 | return ( 79 |
80 |
81 | 82 | Email 83 | 90 | 91 | 92 | 96 | Password 97 | 104 | 105 | 106 | 120 |
121 |
122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/modules/signup/Signup.tsx: -------------------------------------------------------------------------------- 1 | import { Auth } from "../../apiCalls"; 2 | import React from "react"; 3 | import { Redirect } from "react-router"; 4 | import { 5 | FormGroup, 6 | FormControl, 7 | ControlLabel, 8 | Button, 9 | Glyphicon, 10 | HelpBlock, 11 | } from "react-bootstrap"; 12 | import "./signup.css"; 13 | import "./home.css"; 14 | 15 | const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 16 | 17 | interface SignupProps { 18 | isAuthenticated: boolean; 19 | userHasAuthenticated: (authenticated: boolean) => void; 20 | } 21 | 22 | interface SignupState { 23 | loading: boolean; 24 | email: string; 25 | password: string; 26 | confirmPassword: string; 27 | confirmationCode: string; 28 | emailValid: "success" | "error" | "warning" | undefined; 29 | passwordValid: "success" | "error" | "warning" | undefined; 30 | confirmPasswordValid: "success" | "error" | "warning" | undefined; 31 | confirmationCodeValid: "success" | "error" | "warning" | undefined; 32 | user: any; 33 | redirect: boolean; 34 | } 35 | 36 | export default class Signup extends React.Component { 37 | constructor(props: SignupProps) { 38 | super(props); 39 | 40 | this.state = { 41 | loading: false, 42 | email: "", 43 | password: "", 44 | confirmPassword: "", 45 | confirmationCode: "", 46 | emailValid: undefined, 47 | passwordValid: undefined, 48 | confirmPasswordValid: undefined, 49 | confirmationCodeValid: undefined, 50 | user: undefined, 51 | redirect: false, 52 | }; 53 | } 54 | 55 | onEmailChange = (event: React.FormEvent) => { 56 | const target = event.target as HTMLInputElement; 57 | this.setState({ 58 | email: target.value, 59 | emailValid: emailRegex.test(target.value.toLowerCase()) 60 | ? "success" 61 | : "error", 62 | }); 63 | }; 64 | 65 | onPasswordChange = (event: React.FormEvent) => { 66 | const target = event.target as HTMLInputElement; 67 | this.setState({ 68 | password: target.value, 69 | passwordValid: target.value.length < 8 ? "error" : "success", 70 | }); 71 | }; 72 | 73 | onConfirmPasswordChange = (event: React.FormEvent) => { 74 | const target = event.target as HTMLInputElement; 75 | this.setState({ 76 | confirmPassword: target.value, 77 | confirmPasswordValid: 78 | target.value !== this.state.password ? "error" : "success", 79 | }); 80 | }; 81 | 82 | onConfirmationCodeChange = (event: React.FormEvent) => { 83 | const target = event.target as HTMLInputElement; 84 | this.setState({ 85 | confirmationCode: target.value, 86 | confirmationCodeValid: target.value.length > 0 ? "error" : "success", 87 | }); 88 | }; 89 | 90 | onSignup = async (event: React.FormEvent) => { 91 | event.preventDefault(); 92 | this.setState({ loading: true }); 93 | 94 | try { 95 | const user = await Auth.signUp(this.state.email, this.state.password); 96 | this.setState({ user, loading: false }); 97 | } catch (e) { 98 | console.error(e.message); 99 | this.setState({ loading: false }); 100 | } 101 | }; 102 | 103 | onConfirm = async (event: React.FormEvent) => { 104 | event.preventDefault(); 105 | this.setState({ loading: true }); 106 | 107 | try { 108 | // ABHISHEK 109 | // await Auth.confirmSignUp(this.state.email, this.state.confirmationCode); 110 | await Auth.signIn(this.state.email, this.state.password); 111 | this.props.userHasAuthenticated(true); 112 | this.setState({ redirect: true }); 113 | } catch (e) { 114 | console.error(e.message); 115 | this.setState({ loading: false }); 116 | } 117 | }; 118 | 119 | showConfirmationForm = () => { 120 | // if (this.state.redirect) 121 | return ; 122 | 123 | // return ( 124 | //
125 | // 126 | // Confirmation code 127 | // 134 | // 135 | // 136 | // A confirmation code will be sent to the email address provided 137 | // 138 | // 139 | // 150 | //
151 | // ); 152 | }; 153 | 154 | showSignupForm = () => { 155 | return ( 156 |
157 | 158 | Email 159 | 166 | 167 | 168 | 172 | Password 173 | 180 | 181 | Must be at least 8 characters 182 | 183 | 187 | Confirm Password 188 | 195 | 196 | 197 | 212 |
213 | ); 214 | }; 215 | 216 | render() { 217 | return ( 218 |
219 | {this.state.user === undefined 220 | ? this.showSignupForm() 221 | : this.showConfirmationForm()} 222 |
223 | ); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/modules/signup/home.css: -------------------------------------------------------------------------------- 1 | .Home .lander { 2 | padding: 1vh 0; 3 | text-align: left; 4 | } 5 | 6 | .container-category img { 7 | width: 100%; 8 | cursor: pointer; 9 | } 10 | 11 | .Home .product-section { 12 | padding: 30px 0; 13 | } 14 | 15 | .Home .product-section h2 { 16 | padding: 10px 0; 17 | font-family: AmazonEmber, Helvetica Neue, Helvetica, Arial, sans-serif; 18 | font-weight: 300; 19 | text-align: center; 20 | } 21 | 22 | .Home .lander h1 { 23 | font-size: 52px; 24 | text-align: center; 25 | font-family: AmazonEmber, Helvetica Neue, Helvetica, Arial, sans-serif; 26 | font-weight: 300; 27 | } 28 | 29 | .ad-container-padding { 30 | padding: 0 8px 14px 8px !important; 31 | } 32 | 33 | .ad-padding { 34 | padding: 0 7px !important; 35 | } 36 | 37 | .Home .lander p { 38 | color: #555; 39 | font-size: 18px; 40 | line-height: 28px; 41 | /* padding: 40px 15px; */ 42 | font-size: 1.28em; 43 | font-weight: 200; 44 | line-height: 1.6em; 45 | } 46 | /* . { 47 | border: 1px solid; 48 | padding: 10px; 49 | box-shadow: 5px 10px white; 50 | } */ 51 | #footer-button { 52 | border-radius: 60px !important; 53 | } 54 | .img-center { 55 | display: block; 56 | margin-left: auto; 57 | margin-right: auto; 58 | width: 85%; 59 | } 60 | .link-click { 61 | cursor: pointer; 62 | } 63 | .Home a { 64 | font-size: 18px; 65 | line-height: 28px; 66 | font-weight: 200; 67 | line-height: 1.6em; 68 | } 69 | 70 | .Home .product-section p { 71 | padding: 10px 0; 72 | } 73 | 74 | .Home .notes h4 { 75 | font-family: "Open Sans", sans-serif; 76 | font-weight: 600; 77 | overflow: hidden; 78 | line-height: 1.5; 79 | white-space: nowrap; 80 | text-overflow: ellipsis; 81 | } 82 | 83 | .Home .notes p { 84 | color: #666; 85 | } 86 | 87 | .Home h3 a { 88 | color: #005b86; 89 | font-size: 1.2em; 90 | } 91 | 92 | .Home h3 { 93 | color: #005b86; 94 | font-weight: 300; 95 | padding: 0px 0px; 96 | } 97 | 98 | .Home h4 { 99 | font-weight: 500; 100 | padding: 0px 0px; 101 | } 102 | 103 | .Home .button-container { 104 | text-align: center; 105 | padding: 0px 0 60px 0; 106 | } 107 | 108 | .Home .button-container a { 109 | padding: 10px 20px; 110 | background-color: #2eadde; 111 | border: 0; 112 | color: #fff; 113 | font-weight: 400; 114 | font-size: 16px; 115 | } 116 | 117 | .navbar-default { 118 | border: 0px !important; 119 | background: transparent !important; 120 | box-shadow: inset 0 0px 0 rgba(255, 255, 255, 0), 0 1px 5px rgba(0, 0, 0, 0) !important; 121 | -webkit-box-shadow: inset 0 0px 0 rgba(255, 255, 255, 0), 122 | 0 1px 5px rgba(0, 0, 0, 0) !important; 123 | margin: 0 !important; 124 | } 125 | 126 | .spinning.glyphicon { 127 | margin-right: 7px; 128 | top: 2px; 129 | animation: spin 1s infinite linear; 130 | } 131 | 132 | @keyframes spin { 133 | from { 134 | transform: scale(1) rotate(0deg); 135 | } 136 | to { 137 | transform: scale(1) rotate(360deg); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/modules/signup/login.css: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 480px) { 2 | .Login { 3 | padding: 60px 0; 4 | } 5 | 6 | .Login form { 7 | margin: 0 auto; 8 | max-width: 320px; 9 | } 10 | } 11 | 12 | .form-control { 13 | border-radius: 0 !important; 14 | } 15 | 16 | .btn-default{ 17 | background-color: #2EADDE !important; 18 | border-radius: 0 !important; 19 | color: #fff !important; 20 | border: 0; 21 | background-image: none !important; 22 | } 23 | 24 | .btn-default.disabled, .btn-default[disabled], fieldset[disabled] .btn-default, .btn-default.disabled:hover, .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, .btn-default.disabled:focus, .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, .btn-default.disabled.focus, .btn-default[disabled].focus, fieldset[disabled] .btn-default.focus, .btn-default.disabled:active, .btn-default[disabled]:active, fieldset[disabled] .btn-default:active, .btn-default.disabled.active, .btn-default[disabled].active, fieldset[disabled] .btn-default.active{ 25 | background-color: #2EADDE !important; 26 | opacity: 1.00 !important; 27 | } -------------------------------------------------------------------------------- /src/modules/signup/signup.css: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 480px) { 2 | .Signup { 3 | padding: 60px 0; 4 | } 5 | 6 | .Signup form { 7 | margin: 0 auto; 8 | max-width: 320px; 9 | } 10 | } 11 | 12 | .Signup form span.help-block { 13 | font-size: 14px; 14 | padding-bottom: 10px; 15 | color: #999; 16 | } -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.toString()); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl: string) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker) { 62 | installingWorker.onstatechange = () => { 63 | if (installingWorker.state === 'installed') { 64 | if (navigator.serviceWorker.controller) { 65 | // At this point, the old content will have been purged and 66 | // the fresh content will have been added to the cache. 67 | // It's the perfect time to display a "New content is 68 | // available; please refresh." message in your web app. 69 | console.log('New content is available; please refresh.'); 70 | } else { 71 | // At this point, everything has been precached. 72 | // It's the perfect time to display a 73 | // "Content is cached for offline use." message. 74 | console.log('Content is cached for offline use.'); 75 | } 76 | } 77 | }; 78 | } 79 | }; 80 | }) 81 | .catch(error => { 82 | console.error('Error during service worker registration:', error); 83 | }); 84 | } 85 | 86 | function checkValidServiceWorker(swUrl: string) { 87 | // Check if the service worker can be found. If it can't reload the page. 88 | fetch(swUrl) 89 | .then(response => { 90 | // Ensure service worker exists, and that we really are getting a JS file. 91 | if ( 92 | response.status === 404 || 93 | response.headers.get('content-type')!.indexOf('javascript') === -1 94 | ) { 95 | // No service worker found. Probably a different app. Reload the page. 96 | navigator.serviceWorker.ready.then(registration => { 97 | registration.unregister().then(() => { 98 | window.location.reload(); 99 | }); 100 | }); 101 | } else { 102 | // Service worker found. Proceed as normal. 103 | registerValidSW(swUrl); 104 | } 105 | }) 106 | .catch(() => { 107 | console.log( 108 | 'No internet connection found. App is running in offline mode.' 109 | ); 110 | }); 111 | } 112 | 113 | export function unregister() { 114 | if ('serviceWorker' in navigator) { 115 | navigator.serviceWorker.ready.then(registration => { 116 | registration.unregister(); 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": false, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "lib": ["es2015", "es2017", "dom", "esnext.asynciterable"], 17 | "typeRoots": [ 18 | "./src/types", 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "./node_modules/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /workers-site/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | worker 3 | -------------------------------------------------------------------------------- /workers-site/c8qls.js: -------------------------------------------------------------------------------- 1 | const queries = (queryName, bindValue) => { 2 | let queryObj; 3 | switch (queryName) { 4 | case "signup": 5 | queryObj = { 6 | query: `INSERT {_key: @username, password: @passwordHash, customerId: @customerId} INTO UsersTable`, 7 | bindVars: bindValue, 8 | }; 9 | break; 10 | case "AddFriends": 11 | queryObj = { 12 | query: `LET otherUsers = (FOR users in UsersTable FILTER users._key != @username RETURN users) 13 | FOR user in otherUsers 14 | INSERT { _from: CONCAT("UsersTable/",@username), _to: CONCAT("UsersTable/",user._key) } INTO friend`, 15 | bindVars: bindValue, 16 | }; 17 | break; 18 | case "signin": 19 | queryObj = { 20 | query: `FOR user in UsersTable FILTER user._key == @username AND user.password == @passwordHash RETURN user.customerId`, 21 | bindVars: bindValue, 22 | }; 23 | break; 24 | 25 | case "ListBooks": 26 | queryObj = { query: "FOR book IN BooksTable RETURN book", bindVars: {} }; 27 | if (typeof bindValue === "object" && Object.keys(bindValue).length) { 28 | queryObj = { 29 | query: 30 | "FOR book IN BooksTable filter book.category == @category RETURN book", 31 | bindVars: bindValue, 32 | }; 33 | } 34 | 35 | break; 36 | case "GetBook": 37 | queryObj = { 38 | query: "FOR book in BooksTable FILTER book._key == @bookId RETURN book", 39 | bindVars: { bookId: bindValue }, 40 | }; 41 | break; 42 | 43 | case "ListItemsInCart": 44 | queryObj = { 45 | // query: 46 | // "FOR item IN CartTable FILTER item.customerId == @customerId RETURN item", 47 | query: `FOR item IN CartTable FILTER item.customerId == @customerId 48 | FOR book in BooksTable FILTER book._key == item.bookId 49 | RETURN {order: item, book: book}`, 50 | bindVars: bindValue, 51 | }; 52 | break; 53 | case "AddToCart": 54 | queryObj = { 55 | query: `UPSERT { _key: CONCAT_SEPARATOR(":", @customerId, @bookId) } 56 | INSERT { _key: CONCAT_SEPARATOR(":", @customerId, @bookId),customerId: @customerId, bookId: @bookId, quantity: @quantity, price: @price } 57 | UPDATE { quantity: OLD.quantity + @quantity } IN CartTable`, 58 | bindVars: bindValue, 59 | }; 60 | break; 61 | case "UpdateCart": 62 | queryObj = { 63 | query: 64 | 'FOR item IN CartTable UPDATE {_key: CONCAT_SEPARATOR(":", @customerId, @bookId),quantity: @quantity} IN CartTable', 65 | bindVars: bindValue, 66 | }; 67 | break; 68 | case "RemoveFromCart": 69 | queryObj = { 70 | query: 71 | 'REMOVE {_key: CONCAT_SEPARATOR(":", @customerId, @bookId)} IN CartTable', 72 | bindVars: bindValue, 73 | }; 74 | break; 75 | case "GetCartItem": 76 | queryObj = { 77 | query: 78 | "FOR item IN CartTable FILTER item.customerId == @customerId AND item.bookId == @bookId RETURN item", 79 | bindVars: bindValue, 80 | }; 81 | break; 82 | 83 | case "ListOrders": 84 | queryObj = { 85 | query: 86 | "FOR item IN OrdersTable FILTER item.customerId == @customerId RETURN item", 87 | bindVars: bindValue, 88 | }; 89 | break; 90 | case "Checkout": 91 | queryObj = { 92 | query: `LET items = (FOR item IN CartTable FILTER item.customerId == @customerId RETURN item) 93 | LET books = (FOR item in items 94 | FOR book in BooksTable FILTER book._key == item.bookId return {bookId:book._key ,author: book.author,category:book.category,name:book.name,price:book.price,rating:book.rating,quantity:item.quantity}) 95 | INSERT {_key: @orderId, customerId: @customerId, books: books, orderDate: @orderDate} INTO OrdersTable 96 | FOR item IN items REMOVE item IN CartTable`, 97 | bindVars: bindValue, 98 | }; 99 | break; 100 | case "AddPurchased": 101 | queryObj = { 102 | query: `LET order = first(FOR order in OrdersTable FILTER order._key == @orderId RETURN {customerId: order.customerId, books: order.books}) 103 | LET customerId = order.customerId 104 | LET userId = first(FOR user IN UsersTable FILTER user.customerId == customerId RETURN user._id) 105 | LET books = order.books 106 | FOR book IN books 107 | INSERT {_from: userId, _to: CONCAT("BooksTable/",book.bookId)} INTO purchased`, 108 | bindVars: bindValue, 109 | }; 110 | break; 111 | 112 | case "GetBestSellers": 113 | queryObj = { 114 | // query: 115 | // "FOR book in BestsellersTable SORT book.quantity DESC LIMIT 20 return book._key", 116 | query: `FOR bestseller in BestsellersTable 117 | SORT bestseller.quantity DESC 118 | FOR book in BooksTable 119 | FILTER bestseller._key == book._key LIMIT 20 RETURN book`, 120 | bindVars: {}, 121 | }; 122 | break; 123 | 124 | case "GetRecommendations": 125 | queryObj = { 126 | query: `LET userId = first(FOR user in UsersTable FILTER user.customerId == @customerId return user._id) 127 | FOR user IN ANY userId friend 128 | FOR books IN OUTBOUND user purchased 129 | RETURN DISTINCT books`, 130 | bindVars: bindValue, 131 | }; 132 | break; 133 | case "GetRecommendationsByBook": 134 | queryObj = { 135 | query: `LET userId = first(FOR user in UsersTable FILTER user.customerId == @customerId return user._id) 136 | LET bookId = CONCAT("BooksTable/",@bookId) 137 | FOR friendsPurchased IN INBOUND bookId purchased 138 | FOR user IN ANY userId friend 139 | FILTER user._key == friendsPurchased._key 140 | RETURN user`, 141 | bindVars: bindValue, 142 | }; 143 | break; 144 | 145 | case "Search": 146 | queryObj = { 147 | query: `FOR doc IN findBooks 148 | SEARCH PHRASE(doc.name, @search, "text_en") OR PHRASE(doc.author, @search, "text_en") OR PHRASE(doc.category, @search, "text_en") 149 | SORT BM25(doc) desc 150 | RETURN doc`, 151 | bindVars: bindValue, 152 | }; 153 | break; 154 | } 155 | return queryObj; 156 | }; 157 | 158 | module.exports = { queries }; 159 | -------------------------------------------------------------------------------- /workers-site/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getAssetFromKV, 3 | mapRequestToAsset, 4 | } from "@cloudflare/kv-asset-handler"; 5 | const Router = require("./router"); 6 | const init = require("./init"); 7 | const jsc8 = require("jsc8"); 8 | const { queries } = require("./c8qls"); 9 | const { uuid } = require("@cfworker/uuid"); 10 | const { decode } = require("base64-arraybuffer"); 11 | /** 12 | * The DEBUG flag will do two things that help during development: 13 | * 1. we will skip caching on the edge, which makes it easier to 14 | * debug. 15 | * 2. we will return an error message on exception in your Response rather 16 | * than the default 404.html page. 17 | */ 18 | 19 | const DEBUG = false; 20 | 21 | addEventListener("fetch", (event) => { 22 | try { 23 | event.respondWith(handleEvent(event)); 24 | } catch (e) { 25 | if (DEBUG) { 26 | return event.respondWith( 27 | new Response(e.message || e.toString(), { 28 | status: 500, 29 | }) 30 | ); 31 | } 32 | event.respondWith(new Response("Internal Error", { status: 500 })); 33 | } 34 | }); 35 | 36 | async function handleAssetEvent(event) { 37 | const url = new URL(event.request.url); 38 | let options = {}; 39 | 40 | /** 41 | * You can add custom logic to how we fetch your assets 42 | * by configuring the function `mapRequestToAsset` 43 | */ 44 | // options.mapRequestToAsset = handlePrefix(/^\/docs/) 45 | 46 | try { 47 | if (DEBUG) { 48 | // customize caching 49 | options.cacheControl = { 50 | bypassCache: true, 51 | }; 52 | } 53 | return await getAssetFromKV(event, options); 54 | } catch (e) { 55 | // if an error is thrown try to serve the asset at 404.html 56 | if (!DEBUG) { 57 | try { 58 | let notFoundResponse = await getAssetFromKV(event, { 59 | mapRequestToAsset: (req) => 60 | new Request(`${new URL(req.url).origin}/index.html`, req), 61 | }); 62 | 63 | return new Response(notFoundResponse.body, { 64 | ...notFoundResponse, 65 | status: 404, 66 | }); 67 | } catch (e) {} 68 | } 69 | 70 | return new Response(e.message || e.toString(), { status: 500 }); 71 | } 72 | } 73 | 74 | /** 75 | * Here's one example of how to modify a request to 76 | * remove a specific prefix, in this case `/docs` from 77 | * the url. This can be useful if you are deploying to a 78 | * route on a zone, or if you only want your static content 79 | * to exist at a specific path. 80 | */ 81 | function handlePrefix(prefix) { 82 | return (request) => { 83 | // compute the default (e.g. / -> index.html) 84 | let defaultAssetKey = mapRequestToAsset(request); 85 | let url = new URL(defaultAssetKey.url); 86 | 87 | // strip the prefix from the path for lookup 88 | url.pathname = url.pathname.replace(prefix, "/"); 89 | 90 | // inherit all other props from the default request 91 | return new Request(url.toString(), defaultAssetKey); 92 | }; 93 | } 94 | 95 | //////////////////// 96 | 97 | const CUSTOMER_ID_HEADER = "x-customer-id"; 98 | 99 | const optionsObj = { 100 | headers: { 101 | "content-type": "application/json", 102 | }, 103 | }; 104 | 105 | const client = new jsc8({ 106 | url: C8_URL, 107 | fabricName: C8_FABRIC, 108 | apiKey: C8_API_KEY, 109 | agentOptions: { 110 | maxSockets: 50000, 111 | }, 112 | agent: fetch, 113 | }); 114 | 115 | const getLastPathParam = (request) => { 116 | const splitUrl = request.url.split("/"); 117 | return splitUrl[splitUrl.length - 1]; 118 | }; 119 | 120 | const executeQuery = async (c8qlKey, bindValue) => { 121 | const { query, bindVars } = queries(c8qlKey, bindValue); 122 | let result; 123 | try { 124 | result = await client.executeQuery(query, bindVars); 125 | } catch (err) { 126 | result = err; 127 | } 128 | return result; 129 | }; 130 | 131 | const getCustomerId = (request) => request.headers.get(CUSTOMER_ID_HEADER); 132 | 133 | async function initHandler(request) { 134 | let res; 135 | 136 | try { 137 | await init(client); 138 | res = { code: "200", message: "Init successful" }; 139 | } catch (e) { 140 | res = e; 141 | } finally { 142 | return new Response(JSON.stringify(res), optionsObj); 143 | } 144 | } 145 | 146 | async function booksHandler(request, c8qlKey) { 147 | let bindValue = getLastPathParam(request); 148 | if (c8qlKey === "ListBooks" && bindValue.includes("?category=")) { 149 | const queryParam = bindValue.split("?")[1].split("="); 150 | bindValue = { [queryParam[0]]: decodeURI(queryParam[1]) }; 151 | } 152 | const result = await executeQuery(c8qlKey, bindValue); 153 | const body = JSON.stringify(result); 154 | return new Response(body, optionsObj); 155 | } 156 | 157 | async function cartHandler(request, c8qlKey) { 158 | const customerId = getCustomerId(request); 159 | let body = { error: true, code: 400, message: "Customer Id not provided" }; 160 | if (customerId) { 161 | let bindValue = { customerId }; 162 | let requestBody; 163 | if (request.method !== "GET") { 164 | requestBody = await request.json(); 165 | bindValue = { ...bindValue, ...requestBody }; 166 | } else if (c8qlKey === "GetCartItem") { 167 | bindValue = { ...bindValue, bookId: getLastPathParam(request) }; 168 | } 169 | body = await executeQuery(c8qlKey, bindValue); 170 | } 171 | return new Response(JSON.stringify(body), optionsObj); 172 | } 173 | 174 | async function ordersHandler(request, c8qlKey) { 175 | const customerId = getCustomerId(request); 176 | let body = { error: true, code: 400, message: "Customer Id not provided" }; 177 | if (customerId) { 178 | let bindValue = { customerId }; 179 | let orderDate = Date.now(); 180 | const orderId = `${orderDate.toString()}:${customerId}`; 181 | let shouldUpdatePurchased = false; 182 | if (c8qlKey === "Checkout") { 183 | bindValue = { 184 | ...bindValue, 185 | orderId, 186 | orderDate, 187 | }; 188 | shouldUpdatePurchased = true; 189 | } 190 | body = await executeQuery(c8qlKey, bindValue); 191 | if (shouldUpdatePurchased && !body.error) { 192 | await executeQuery("AddPurchased", { orderId }); 193 | } 194 | } 195 | return new Response(JSON.stringify(body), optionsObj); 196 | } 197 | 198 | async function bestSellersHandler(request, c8qlKey) { 199 | const result = await executeQuery(c8qlKey); 200 | return new Response(JSON.stringify(result), optionsObj); 201 | } 202 | 203 | async function recommendationsHandler(request, c8qlKey) { 204 | const customerId = getCustomerId(request); 205 | let body = { error: true, code: 400, message: "Customer Id not provided" }; 206 | if (customerId) { 207 | let bindValue = { customerId }; 208 | if (c8qlKey === "GetRecommendationsByBook") { 209 | const bookId = getLastPathParam(request); 210 | bindValue = { ...bindValue, bookId }; 211 | } 212 | body = await executeQuery(c8qlKey, bindValue); 213 | } 214 | return new Response(JSON.stringify(body), optionsObj); 215 | } 216 | 217 | async function searchHandler(request, c8qlKey) { 218 | const queryParam = getLastPathParam(request); 219 | const search = decodeURIComponent(queryParam.split("?")[1].split("=")[1]); 220 | const body = await executeQuery(c8qlKey, { search }); 221 | return new Response(JSON.stringify(body), optionsObj); 222 | } 223 | 224 | async function signupHandler(request) { 225 | const { username, password } = await request.json(); 226 | 227 | const encodedPassword = new TextEncoder().encode(password); 228 | 229 | const digestedPassword = await crypto.subtle.digest( 230 | { 231 | name: "SHA-256", 232 | }, 233 | encodedPassword // The data you want to hash as an ArrayBuffer 234 | ); 235 | const passwordHash = new TextDecoder("utf-8").decode(digestedPassword); 236 | const customerId = uuid(); 237 | const result = await executeQuery("signup", { 238 | username, 239 | passwordHash, 240 | customerId, 241 | }); 242 | if (!result.error) { 243 | const res = await executeQuery("AddFriends", { username }); 244 | } 245 | 246 | const body = JSON.stringify(result); 247 | return new Response(body, optionsObj); 248 | } 249 | 250 | async function signinHandler(request) { 251 | const { username, password } = await request.json(); 252 | const encodedPassword = new TextEncoder().encode(password); 253 | const digestedPassword = await crypto.subtle.digest( 254 | { 255 | name: "SHA-256", 256 | }, 257 | encodedPassword // The data you want to hash as an ArrayBuffer 258 | ); 259 | const passwordHash = new TextDecoder("utf-8").decode(digestedPassword); 260 | const result = await executeQuery("signin", { 261 | username, 262 | passwordHash, 263 | }); 264 | let message = "User not found"; 265 | let status = 404; 266 | if (result.length) { 267 | message = result; 268 | status = 200; 269 | } 270 | const body = JSON.stringify({ message }); 271 | return new Response(body, { status, ...optionsObj }); 272 | } 273 | 274 | async function whoAmIHandler(request) { 275 | const customerId = getCustomerId(request); 276 | let message = "No current user"; 277 | let status = 500; 278 | if (customerId !== "null" && customerId) { 279 | message = customerId; 280 | status = 200; 281 | } 282 | return new Response(JSON.stringify({ message }), { status, ...optionsObj }); 283 | } 284 | 285 | async function getImageHandler(request) { 286 | const queryParam = getLastPathParam(request); 287 | const bookId = queryParam.split("?")[1].split("=")[1]; 288 | // const res = await client.getValueForKey("ImagesKVTable", bookId); 289 | // const base64Img = res.value; 290 | // const response = new Response(decode(base64Img), { 291 | // headers: { "Content-Type": "image/jpeg" }, 292 | // }); 293 | 294 | const res = await BOOK_IMAGES.get(bookId, "arrayBuffer"); 295 | const response = new Response(res, { 296 | headers: { "Content-Type": "image/jpeg" }, 297 | }); 298 | 299 | return response; 300 | } 301 | 302 | async function handleEvent(event) { 303 | const { request } = event; 304 | const r = new Router(); 305 | 306 | r.post(".*/api/init", (request) => initHandler(request)); 307 | 308 | r.get(".*/api/whoami", (request) => whoAmIHandler(request)); 309 | 310 | r.post(".*/api/signup", (request) => signupHandler(request)); 311 | 312 | r.post(".*/api/signin", (request) => signinHandler(request)); 313 | 314 | r.get(".*/api/books*", (request) => booksHandler(request, "ListBooks")); 315 | r.get(".*/api/books/b[0-9]+", (request) => booksHandler(request, "GetBook")); 316 | 317 | r.get(".*/api/cart", (request) => cartHandler(request, "ListItemsInCart")); 318 | r.get(".*/api/cart/b[0-9]+", (request) => 319 | cartHandler(request, "GetCartItem") 320 | ); 321 | r.post(".*/api/cart", (request) => cartHandler(request, "AddToCart")); 322 | r.put(".*/api/cart", (request) => cartHandler(request, "UpdateCart")); 323 | r.delete(".*/api/cart", (request) => cartHandler(request, "RemoveFromCart")); 324 | 325 | r.get(".*/api/orders", (request) => ordersHandler(request, "ListOrders")); 326 | // add all books from the Cart table to the Orders table 327 | // remove all entries from the Cart table for the requested customer ID 328 | r.post(".*/api/orders", (request) => ordersHandler(request, "Checkout")); 329 | 330 | r.get(".*/api/bestsellers", (request) => 331 | bestSellersHandler(request, "GetBestSellers") 332 | ); 333 | 334 | r.get(".*/api/recommendations", (request) => 335 | recommendationsHandler(request, "GetRecommendations") 336 | ); 337 | r.get(".*/api/recommendations/b[0-9]+", (request) => 338 | recommendationsHandler(request, "GetRecommendationsByBook") 339 | ); 340 | 341 | r.get(".*/api/getImage*", (request) => getImageHandler(request)); 342 | 343 | r.get(".*/api/search", (request) => searchHandler(request, "Search")); 344 | 345 | r.get("/.*", () => handleAssetEvent(event)); 346 | 347 | const resp = await r.route(request); 348 | return resp; 349 | } 350 | -------------------------------------------------------------------------------- /workers-site/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "worker", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@cfworker/uuid": "^1.5.0", 13 | "@cloudflare/kv-asset-handler": "~0.0.11", 14 | "base64-arraybuffer": "^0.2.0", 15 | "jsc8": "^0.16.3" 16 | } 17 | }, 18 | "node_modules/@cfworker/uuid": { 19 | "version": "1.5.0", 20 | "resolved": "https://registry.npmjs.org/@cfworker/uuid/-/uuid-1.5.0.tgz", 21 | "integrity": "sha512-VQsDohZfglzXrbYllc51e/vja/0bb70m4ii5q/tEFhMKJE5GHUiunIUFtrSrC8h7qy/LhNYIl7ZMaqooqA+JQQ==" 22 | }, 23 | "node_modules/@cloudflare/kv-asset-handler": { 24 | "version": "0.0.11", 25 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.0.11.tgz", 26 | "integrity": "sha512-D2kGr8NF2Er//Mx0c4+8FtOHuLrnwOlpC48TbtyxRSegG/Js15OKoqxxlG9BMUj3V/YSqtN8bUU6pjaRlsoSqg==", 27 | "dependencies": { 28 | "@cloudflare/workers-types": "^2.0.0", 29 | "@types/mime": "^2.0.2", 30 | "mime": "^2.4.6" 31 | } 32 | }, 33 | "node_modules/@cloudflare/workers-types": { 34 | "version": "2.0.0", 35 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-2.0.0.tgz", 36 | "integrity": "sha512-SFUPQzR5aV2TBLP4Re+xNX5KfAGArcRGA44OLulBDnfblEf3J+6kFvdJAQwFhFpqru3wImwT1cX0wahk6EeWTw==" 37 | }, 38 | "node_modules/@types/mime": { 39 | "version": "2.0.2", 40 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", 41 | "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" 42 | }, 43 | "node_modules/async-limiter": { 44 | "version": "1.0.1", 45 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 46 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 47 | }, 48 | "node_modules/base64-arraybuffer": { 49 | "version": "0.2.0", 50 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", 51 | "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", 52 | "engines": { 53 | "node": ">= 0.6.0" 54 | } 55 | }, 56 | "node_modules/bluebird": { 57 | "version": "3.7.2", 58 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 59 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 60 | }, 61 | "node_modules/csvtojson": { 62 | "version": "2.0.10", 63 | "resolved": "https://registry.npmjs.org/csvtojson/-/csvtojson-2.0.10.tgz", 64 | "integrity": "sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==", 65 | "dependencies": { 66 | "bluebird": "^3.5.1", 67 | "lodash": "^4.17.3", 68 | "strip-bom": "^2.0.0" 69 | }, 70 | "bin": { 71 | "csvtojson": "bin/csvtojson" 72 | }, 73 | "engines": { 74 | "node": ">=4.0.0" 75 | } 76 | }, 77 | "node_modules/decode-uri-component": { 78 | "version": "0.2.0", 79 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", 80 | "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", 81 | "engines": { 82 | "node": ">=0.10" 83 | } 84 | }, 85 | "node_modules/dom-walk": { 86 | "version": "0.1.2", 87 | "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", 88 | "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" 89 | }, 90 | "node_modules/es6-error": { 91 | "version": "4.1.1", 92 | "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", 93 | "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" 94 | }, 95 | "node_modules/file-type": { 96 | "version": "4.4.0", 97 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", 98 | "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", 99 | "engines": { 100 | "node": ">=4" 101 | } 102 | }, 103 | "node_modules/global": { 104 | "version": "4.3.2", 105 | "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", 106 | "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", 107 | "dependencies": { 108 | "min-document": "^2.19.0", 109 | "process": "~0.5.1" 110 | } 111 | }, 112 | "node_modules/is-function": { 113 | "version": "1.0.2", 114 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", 115 | "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" 116 | }, 117 | "node_modules/is-utf8": { 118 | "version": "0.2.1", 119 | "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", 120 | "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" 121 | }, 122 | "node_modules/jsc8": { 123 | "version": "0.16.3", 124 | "resolved": "https://registry.npmjs.org/jsc8/-/jsc8-0.16.3.tgz", 125 | "integrity": "sha512-g43BunIHCAWDjEz0kajyG4newzsP+xv0U4QszMfMxeJ91/4L2itFk/K2QdQuC4VZzhF9+WtnsQBlOY9umH+Rcg==", 126 | "dependencies": { 127 | "csvtojson": "^2.0.10", 128 | "es6-error": "^4.0.1", 129 | "jwt-decode": "^2.2.0", 130 | "linkedlist": "^1.0.1", 131 | "multi-part": "^2.0.0", 132 | "query-string": "^6.11.1", 133 | "ws": "^6.1.0", 134 | "xhr": "^2.4.1" 135 | } 136 | }, 137 | "node_modules/jwt-decode": { 138 | "version": "2.2.0", 139 | "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", 140 | "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" 141 | }, 142 | "node_modules/linkedlist": { 143 | "version": "1.0.1", 144 | "resolved": "https://registry.npmjs.org/linkedlist/-/linkedlist-1.0.1.tgz", 145 | "integrity": "sha1-e3QYm/rW52Nn+1oQ88NpExKLeCs=", 146 | "engines": { 147 | "node": ">= v0.6.x" 148 | } 149 | }, 150 | "node_modules/lodash": { 151 | "version": "4.17.20", 152 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 153 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 154 | }, 155 | "node_modules/mime": { 156 | "version": "2.4.6", 157 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 158 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", 159 | "bin": { 160 | "mime": "cli.js" 161 | }, 162 | "engines": { 163 | "node": ">=4.0.0" 164 | } 165 | }, 166 | "node_modules/mime-db": { 167 | "version": "1.44.0", 168 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 169 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", 170 | "engines": { 171 | "node": ">= 0.6" 172 | } 173 | }, 174 | "node_modules/mime-kind": { 175 | "version": "2.0.2", 176 | "resolved": "https://registry.npmjs.org/mime-kind/-/mime-kind-2.0.2.tgz", 177 | "integrity": "sha1-WkPVvr3rCCGCIk2dJjIGMp5Xzfg=", 178 | "dependencies": { 179 | "file-type": "^4.3.0", 180 | "mime-types": "^2.1.15" 181 | }, 182 | "engines": { 183 | "node": ">=4.0.0" 184 | } 185 | }, 186 | "node_modules/mime-types": { 187 | "version": "2.1.27", 188 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 189 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 190 | "dependencies": { 191 | "mime-db": "1.44.0" 192 | }, 193 | "engines": { 194 | "node": ">= 0.6" 195 | } 196 | }, 197 | "node_modules/min-document": { 198 | "version": "2.19.0", 199 | "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", 200 | "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", 201 | "dependencies": { 202 | "dom-walk": "^0.1.0" 203 | } 204 | }, 205 | "node_modules/multi-part": { 206 | "version": "2.0.0", 207 | "resolved": "https://registry.npmjs.org/multi-part/-/multi-part-2.0.0.tgz", 208 | "integrity": "sha1-Z09TtDL4UM+MwC0w0h8gZOMJVjw=", 209 | "dependencies": { 210 | "mime-kind": "^2.0.1" 211 | }, 212 | "engines": { 213 | "node": ">=4.0.0" 214 | } 215 | }, 216 | "node_modules/parse-headers": { 217 | "version": "2.0.3", 218 | "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", 219 | "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==" 220 | }, 221 | "node_modules/process": { 222 | "version": "0.5.2", 223 | "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", 224 | "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=", 225 | "engines": { 226 | "node": ">= 0.6.0" 227 | } 228 | }, 229 | "node_modules/query-string": { 230 | "version": "6.13.6", 231 | "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.6.tgz", 232 | "integrity": "sha512-/WWZ7d9na6s2wMEGdVCVgKWE9Rt7nYyNIf7k8xmHXcesPMlEzicWo3lbYwHyA4wBktI2KrXxxZeACLbE84hvSQ==", 233 | "dependencies": { 234 | "decode-uri-component": "^0.2.0", 235 | "split-on-first": "^1.0.0", 236 | "strict-uri-encode": "^2.0.0" 237 | }, 238 | "engines": { 239 | "node": ">=6" 240 | } 241 | }, 242 | "node_modules/split-on-first": { 243 | "version": "1.1.0", 244 | "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", 245 | "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", 246 | "engines": { 247 | "node": ">=6" 248 | } 249 | }, 250 | "node_modules/strict-uri-encode": { 251 | "version": "2.0.0", 252 | "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", 253 | "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", 254 | "engines": { 255 | "node": ">=4" 256 | } 257 | }, 258 | "node_modules/strip-bom": { 259 | "version": "2.0.0", 260 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", 261 | "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", 262 | "dependencies": { 263 | "is-utf8": "^0.2.0" 264 | }, 265 | "engines": { 266 | "node": ">=0.10.0" 267 | } 268 | }, 269 | "node_modules/ws": { 270 | "version": "6.2.1", 271 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", 272 | "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", 273 | "dependencies": { 274 | "async-limiter": "~1.0.0" 275 | } 276 | }, 277 | "node_modules/xhr": { 278 | "version": "2.5.0", 279 | "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", 280 | "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", 281 | "dependencies": { 282 | "global": "~4.3.0", 283 | "is-function": "^1.0.1", 284 | "parse-headers": "^2.0.0", 285 | "xtend": "^4.0.0" 286 | } 287 | }, 288 | "node_modules/xtend": { 289 | "version": "4.0.2", 290 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 291 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 292 | "engines": { 293 | "node": ">=0.4" 294 | } 295 | } 296 | }, 297 | "dependencies": { 298 | "@cfworker/uuid": { 299 | "version": "1.5.0", 300 | "resolved": "https://registry.npmjs.org/@cfworker/uuid/-/uuid-1.5.0.tgz", 301 | "integrity": "sha512-VQsDohZfglzXrbYllc51e/vja/0bb70m4ii5q/tEFhMKJE5GHUiunIUFtrSrC8h7qy/LhNYIl7ZMaqooqA+JQQ==" 302 | }, 303 | "@cloudflare/kv-asset-handler": { 304 | "version": "0.0.11", 305 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.0.11.tgz", 306 | "integrity": "sha512-D2kGr8NF2Er//Mx0c4+8FtOHuLrnwOlpC48TbtyxRSegG/Js15OKoqxxlG9BMUj3V/YSqtN8bUU6pjaRlsoSqg==", 307 | "requires": { 308 | "@cloudflare/workers-types": "^2.0.0", 309 | "@types/mime": "^2.0.2", 310 | "mime": "^2.4.6" 311 | } 312 | }, 313 | "@cloudflare/workers-types": { 314 | "version": "2.0.0", 315 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-2.0.0.tgz", 316 | "integrity": "sha512-SFUPQzR5aV2TBLP4Re+xNX5KfAGArcRGA44OLulBDnfblEf3J+6kFvdJAQwFhFpqru3wImwT1cX0wahk6EeWTw==" 317 | }, 318 | "@types/mime": { 319 | "version": "2.0.2", 320 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", 321 | "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" 322 | }, 323 | "async-limiter": { 324 | "version": "1.0.1", 325 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 326 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 327 | }, 328 | "base64-arraybuffer": { 329 | "version": "0.2.0", 330 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", 331 | "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==" 332 | }, 333 | "bluebird": { 334 | "version": "3.7.2", 335 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 336 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 337 | }, 338 | "csvtojson": { 339 | "version": "2.0.10", 340 | "resolved": "https://registry.npmjs.org/csvtojson/-/csvtojson-2.0.10.tgz", 341 | "integrity": "sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==", 342 | "requires": { 343 | "bluebird": "^3.5.1", 344 | "lodash": "^4.17.3", 345 | "strip-bom": "^2.0.0" 346 | } 347 | }, 348 | "decode-uri-component": { 349 | "version": "0.2.0", 350 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", 351 | "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" 352 | }, 353 | "dom-walk": { 354 | "version": "0.1.2", 355 | "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", 356 | "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" 357 | }, 358 | "es6-error": { 359 | "version": "4.1.1", 360 | "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", 361 | "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" 362 | }, 363 | "file-type": { 364 | "version": "4.4.0", 365 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", 366 | "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=" 367 | }, 368 | "global": { 369 | "version": "4.3.2", 370 | "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", 371 | "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", 372 | "requires": { 373 | "min-document": "^2.19.0", 374 | "process": "~0.5.1" 375 | } 376 | }, 377 | "is-function": { 378 | "version": "1.0.2", 379 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", 380 | "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" 381 | }, 382 | "is-utf8": { 383 | "version": "0.2.1", 384 | "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", 385 | "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" 386 | }, 387 | "jsc8": { 388 | "version": "0.16.3", 389 | "resolved": "https://registry.npmjs.org/jsc8/-/jsc8-0.16.3.tgz", 390 | "integrity": "sha512-g43BunIHCAWDjEz0kajyG4newzsP+xv0U4QszMfMxeJ91/4L2itFk/K2QdQuC4VZzhF9+WtnsQBlOY9umH+Rcg==", 391 | "requires": { 392 | "csvtojson": "^2.0.10", 393 | "es6-error": "^4.0.1", 394 | "jwt-decode": "^2.2.0", 395 | "linkedlist": "^1.0.1", 396 | "multi-part": "^2.0.0", 397 | "query-string": "^6.11.1", 398 | "ws": "^6.1.0", 399 | "xhr": "^2.4.1" 400 | } 401 | }, 402 | "jwt-decode": { 403 | "version": "2.2.0", 404 | "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", 405 | "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" 406 | }, 407 | "linkedlist": { 408 | "version": "1.0.1", 409 | "resolved": "https://registry.npmjs.org/linkedlist/-/linkedlist-1.0.1.tgz", 410 | "integrity": "sha1-e3QYm/rW52Nn+1oQ88NpExKLeCs=" 411 | }, 412 | "lodash": { 413 | "version": "4.17.20", 414 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 415 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 416 | }, 417 | "mime": { 418 | "version": "2.4.6", 419 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 420 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" 421 | }, 422 | "mime-db": { 423 | "version": "1.44.0", 424 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 425 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 426 | }, 427 | "mime-kind": { 428 | "version": "2.0.2", 429 | "resolved": "https://registry.npmjs.org/mime-kind/-/mime-kind-2.0.2.tgz", 430 | "integrity": "sha1-WkPVvr3rCCGCIk2dJjIGMp5Xzfg=", 431 | "requires": { 432 | "file-type": "^4.3.0", 433 | "mime-types": "^2.1.15" 434 | } 435 | }, 436 | "mime-types": { 437 | "version": "2.1.27", 438 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 439 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 440 | "requires": { 441 | "mime-db": "1.44.0" 442 | } 443 | }, 444 | "min-document": { 445 | "version": "2.19.0", 446 | "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", 447 | "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", 448 | "requires": { 449 | "dom-walk": "^0.1.0" 450 | } 451 | }, 452 | "multi-part": { 453 | "version": "2.0.0", 454 | "resolved": "https://registry.npmjs.org/multi-part/-/multi-part-2.0.0.tgz", 455 | "integrity": "sha1-Z09TtDL4UM+MwC0w0h8gZOMJVjw=", 456 | "requires": { 457 | "mime-kind": "^2.0.1" 458 | } 459 | }, 460 | "parse-headers": { 461 | "version": "2.0.3", 462 | "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", 463 | "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==" 464 | }, 465 | "process": { 466 | "version": "0.5.2", 467 | "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", 468 | "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" 469 | }, 470 | "query-string": { 471 | "version": "6.13.6", 472 | "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.6.tgz", 473 | "integrity": "sha512-/WWZ7d9na6s2wMEGdVCVgKWE9Rt7nYyNIf7k8xmHXcesPMlEzicWo3lbYwHyA4wBktI2KrXxxZeACLbE84hvSQ==", 474 | "requires": { 475 | "decode-uri-component": "^0.2.0", 476 | "split-on-first": "^1.0.0", 477 | "strict-uri-encode": "^2.0.0" 478 | } 479 | }, 480 | "split-on-first": { 481 | "version": "1.1.0", 482 | "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", 483 | "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" 484 | }, 485 | "strict-uri-encode": { 486 | "version": "2.0.0", 487 | "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", 488 | "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" 489 | }, 490 | "strip-bom": { 491 | "version": "2.0.0", 492 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", 493 | "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", 494 | "requires": { 495 | "is-utf8": "^0.2.0" 496 | } 497 | }, 498 | "ws": { 499 | "version": "6.2.1", 500 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", 501 | "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", 502 | "requires": { 503 | "async-limiter": "~1.0.0" 504 | } 505 | }, 506 | "xhr": { 507 | "version": "2.5.0", 508 | "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", 509 | "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", 510 | "requires": { 511 | "global": "~4.3.0", 512 | "is-function": "^1.0.1", 513 | "parse-headers": "^2.0.0", 514 | "xtend": "^4.0.0" 515 | } 516 | }, 517 | "xtend": { 518 | "version": "4.0.2", 519 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 520 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 521 | } 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /workers-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "worker", 4 | "version": "1.0.0", 5 | "description": "A template for kick starting a Cloudflare Workers project", 6 | "main": "index.js", 7 | "author": "Ashley Lewis ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@cfworker/uuid": "^1.5.0", 11 | "@cloudflare/kv-asset-handler": "~0.0.11", 12 | "base64-arraybuffer": "^0.2.0", 13 | "jsc8": "^0.16.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /workers-site/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper functions that when passed a request will return a 3 | * boolean indicating if the request uses that HTTP method, 4 | * header, host or referrer. 5 | */ 6 | const Method = (method) => (req) => 7 | req.method.toLowerCase() === method.toLowerCase() 8 | const Connect = Method('connect') 9 | const Delete = Method('delete') 10 | const Get = Method('get') 11 | const Head = Method('head') 12 | const Options = Method('options') 13 | const Patch = Method('patch') 14 | const Post = Method('post') 15 | const Put = Method('put') 16 | const Trace = Method('trace') 17 | 18 | // const Header = (header, val) => (req) => req.headers.get(header) === val 19 | // const Host = (host) => Header('host', host.toLowerCase()) 20 | // const Referrer = (host) => Header('referrer', host.toLowerCase()) 21 | 22 | const Path = (regExp) => (req) => { 23 | const url = new URL(req.url) 24 | const path = url.pathname 25 | const match = path.match(regExp) || [] 26 | return match[0] === path 27 | } 28 | 29 | /** 30 | * The Router handles determines which handler is matched given the 31 | * conditions present for each request. 32 | */ 33 | class Router { 34 | constructor() { 35 | this.routes = [] 36 | } 37 | 38 | handle(conditions, handler) { 39 | this.routes.push({ 40 | conditions, 41 | handler, 42 | }) 43 | return this 44 | } 45 | 46 | connect(url, handler) { 47 | return this.handle([Connect, Path(url)], handler) 48 | } 49 | 50 | delete(url, handler) { 51 | return this.handle([Delete, Path(url)], handler) 52 | } 53 | 54 | get(url, handler) { 55 | return this.handle([Get, Path(url)], handler) 56 | } 57 | 58 | head(url, handler) { 59 | return this.handle([Head, Path(url)], handler) 60 | } 61 | 62 | options(url, handler) { 63 | return this.handle([Options, Path(url)], handler) 64 | } 65 | 66 | patch(url, handler) { 67 | return this.handle([Patch, Path(url)], handler) 68 | } 69 | 70 | post(url, handler) { 71 | return this.handle([Post, Path(url)], handler) 72 | } 73 | 74 | put(url, handler) { 75 | return this.handle([Put, Path(url)], handler) 76 | } 77 | 78 | trace(url, handler) { 79 | return this.handle([Trace, Path(url)], handler) 80 | } 81 | 82 | all(handler) { 83 | return this.handle([], handler) 84 | } 85 | 86 | route(req) { 87 | const route = this.resolve(req) 88 | 89 | if (route) { 90 | return route.handler(req) 91 | } 92 | 93 | return new Response('resource not found', { 94 | status: 404, 95 | statusText: 'not found', 96 | headers: { 97 | 'content-type': 'text/plain', 98 | }, 99 | }) 100 | } 101 | 102 | /** 103 | * resolve returns the matching route for a request that returns 104 | * true for all conditions (if any). 105 | */ 106 | resolve(req) { 107 | return this.routes.find((r) => { 108 | if (!r.conditions || (Array.isArray(r) && !r.conditions.length)) { 109 | return true 110 | } 111 | 112 | if (typeof r.conditions === 'function') { 113 | return r.conditions(req) 114 | } 115 | 116 | return r.conditions.every((c) => c(req)) 117 | }) 118 | } 119 | } 120 | 121 | module.exports = Router 122 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "bookstore" 2 | type = "webpack" 3 | account_id = "xxxx" 4 | workers_dev = false 5 | # The ID of the domain to deploying to 6 | zone_id = "xxxx" 7 | 8 | # The route pattern your Workers application will be served at 9 | route = "bookstore.macrometa.io/*" 10 | 11 | vars = { DC_LIST="xxxxx", C8_URL="xxxx", C8_FABRIC="xxxx", C8_API_KEY="xxxx" } 12 | kv-namespaces = [ 13 | { binding = "BOOK_IMAGES", id = "XXXX", preview_id="xxxxx" } 14 | ] 15 | [site] 16 | bucket = "./build" 17 | entry-point = "workers-site" 18 | --------------------------------------------------------------------------------