├── .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 | 
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 | 
32 |
33 | ## Data & Control Flows
34 |
35 | 
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 | 
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 | EDGE COMMERCE DEMO
204 |
205 |
206 |
207 |
208 |
209 | {this.state.isAuthenticated ? (
210 | <>
211 |
212 |
217 |
225 | View Latency Stats
226 |
227 |
228 | {/*
232 | Latency :
233 |
234 |
239 | {this.state.showLatestNetworkLatencyValue}
240 | */}
241 |
242 | >
243 | ) : null}
244 |
245 |
246 | {this.state.isAuthenticated
247 | ? this.showLoggedInBar()
248 | : this.showLoggedOutBar()}
249 |
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 |
83 | {this.state.loading && (
84 |
85 | )}
86 | {this.props.variant === "buyAgain"
87 | ? `Buy again`
88 | : this.state.buttonText}
89 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
116 | {this.state.removeLoading && (
117 |
118 | )}
119 | Remove
120 |
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 |
107 | Checkout
108 |
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 |
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 |
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 |
9 | {Object.values(categories).map((category) => (
10 |
11 |
17 | {category}
18 |
19 |
20 | ))}
21 |
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 | View Receipt
47 |
48 |
49 |
50 |
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 |
95 |
131 |
132 |
133 |
134 | {`Pay ($${this.getOrderTotal()})`}
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 |
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 |
50 | );
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 |
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 |
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 |
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 |
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 |
188 |
189 | );
190 | }
191 |
192 | renderHome() {
193 | return (
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
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 |
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 | //
151 | // );
152 | };
153 |
154 | showSignupForm = () => {
155 | return (
156 |
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 |
--------------------------------------------------------------------------------