This is page 1 of 2. Use http://codebase.md/patrice-truong/cosmosdb-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── assets
│ ├── architecture.drawio
│ ├── architecture.png
│ ├── constants_email.png
│ ├── cosmos-add-australia-east.png
│ ├── cosmos-add-region.png
│ ├── cosmos-backup-policy.png
│ ├── cosmos-basics.png
│ ├── cosmos-create-carts.png
│ ├── cosmos-create-products.png
│ ├── cosmos-create.png
│ ├── cosmos-enable-vector-search.png
│ ├── cosmos-encryption.png
│ ├── cosmos-global-distribution.png
│ ├── cosmos-helper-australia.png
│ ├── cosmos-multi-regions-write.png
│ ├── cosmos-networking.png
│ ├── cosmos-new-container.png
│ ├── cosmos-populate-products.png
│ ├── cosmos-products.png
│ ├── cosmos-rbac.png
│ ├── cosmos-validation.png
│ ├── demo.gif
│ ├── demo.png
│ ├── dotnet_build.png
│ ├── npm_run_build.png
│ ├── socket_server_ip.png
│ ├── storage-access-control.png
│ ├── storage-advanced.png
│ ├── storage-basics.png
│ ├── storage-blob-data-contributor.png
│ ├── storage-create-container.png
│ ├── storage-data-protection.png
│ ├── storage-encryption.png
│ ├── storage-files-uploaded.png
│ ├── storage-networking.png
│ ├── storage-review.png
│ ├── storage-role-assignment.png
│ ├── storage-upload-files.png
│ ├── storage-upload.png
│ ├── storage-validation.png
│ ├── vm-advanced.png
│ ├── vm-basics.png
│ ├── vm-disks.png
│ ├── vm-management.png
│ ├── vm-monitoring.png
│ ├── vm-networking.png
│ ├── vm-title.png
│ ├── vm-validation.png
│ └── vm-workload.png
├── LICENSE
├── mcp-server
│ ├── .env.template
│ ├── package-lock.json
│ ├── package.json
│ ├── run_mcp_server.cmd
│ ├── src
│ │ ├── aoai.ts
│ │ ├── cosmosdb-mcp-server.ts
│ │ ├── cosmosdb.ts
│ │ └── server.ts
│ └── tsconfig.json
├── nextjs
│ ├── .env.template
│ ├── .eslintrc.json
│ ├── app
│ │ ├── api
│ │ │ ├── blob-url
│ │ │ │ └── route.ts
│ │ │ ├── cart
│ │ │ │ └── route.ts
│ │ │ ├── chat
│ │ │ │ └── route.ts
│ │ │ ├── orders
│ │ │ │ ├── [id]
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ └── products
│ │ │ └── route.ts
│ │ ├── cart
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── orders
│ │ │ └── [id]
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ └── products
│ │ └── page.tsx
│ ├── components
│ │ ├── AIAssistantDrawer.tsx
│ │ ├── CartItemCount.tsx
│ │ ├── Products.tsx
│ │ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
│ ├── components.json
│ ├── context
│ │ └── CartContext.tsx
│ ├── hooks
│ │ └── use-toast.ts
│ ├── lib
│ │ ├── cosmosdb.ts
│ │ ├── mcp-client.ts
│ │ └── utils.ts
│ ├── models
│ │ ├── cart.ts
│ │ ├── cartContextType.ts
│ │ ├── cartItem.ts
│ │ ├── constants.ts
│ │ └── product.ts
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ └── placeholder-300x300.png
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── populate
│ ├── catalog.json
│ ├── populate.csproj
│ ├── product.cs
│ ├── program.cs
│ └── set_rbac.ps1
└── README.md
```
# Files
--------------------------------------------------------------------------------
/nextjs/.eslintrc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
```
--------------------------------------------------------------------------------
/mcp-server/.env.template:
--------------------------------------------------------------------------------
```
1 | AZURE_COSMOSDB_NOSQL_ENDPOINT=https://<cosmosdb_account>.documents.azure.com:443/
2 | AZURE_COSMOSDB_NOSQL_DATABASE=eshop
3 | AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER=products
4 | AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER=carts
5 | AZURE_COSMOSDB_NOSQL_ORDERS_CONTAINER=orders
6 |
7 | NEXT_PUBLIC_AZURE_TENANT_ID=<tenant_id>
8 | NEXT_PUBLIC_AZURE_CLIENT_ID=<client_id>
9 | NEXT_PUBLIC_AZURE_CLIENT_SECRET=<client_secret>
10 |
11 | NEXT_PUBLIC_AZURE_STORAGE_ACCOUNT_NAME=<storage_account_name>
12 | NEXT_PUBLIC_AZURE_STORAGE_CONTAINER_NAME=img
13 |
14 | AZURE_OPENAI_ENDPOINT=https://<azure_openai_account>.openai.azure.com/
15 | AZURE_OPENAI_API_KEY=<azure_openai_key>
16 | AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-3-small
17 | AZURE_OPENAI_API_VERSION=2024-05-01-preview
```
--------------------------------------------------------------------------------
/nextjs/.env.template:
--------------------------------------------------------------------------------
```
1 | AZURE_COSMOSDB_NOSQL_ENDPOINT=https://<cosmosdb_account>.documents.azure.com:443/
2 | AZURE_COSMOSDB_NOSQL_DATABASE=eshop
3 | AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER=products
4 | AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER=carts
5 | AZURE_COSMOSDB_NOSQL_ORDERS_CONTAINER=orders
6 |
7 | NEXT_PUBLIC_AZURE_TENANT_ID=<tenant_id>
8 | NEXT_PUBLIC_AZURE_CLIENT_ID=<client_id>
9 | NEXT_PUBLIC_AZURE_CLIENT_SECRET=<client_secret>
10 |
11 | NEXT_PUBLIC_AZURE_STORAGE_ACCOUNT_NAME=<storage_account_name>
12 | NEXT_PUBLIC_AZURE_STORAGE_CONTAINER_NAME=img
13 |
14 | AZURE_OPENAI_ENDPOINT=https://<azure_openai_account>.openai.azure.com/
15 | AZURE_OPENAI_API_KEY=<azure_openai_key>
16 | AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-3-small
17 | AZURE_OPENAI_API_VERSION=2024-05-01-preview
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 |
2 | # Created by https://www.gitignore.io/api/visualstudio
3 |
4 | ### VisualStudio ###
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 | ##
8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
9 |
10 | *.dll
11 | *.pdb
12 | *.cachefile
13 | *.bak
14 | *.ide*
15 | .terraform/
16 | .angular
17 | .next
18 | cache
19 |
20 | # User-specific files
21 | *.suo
22 | *.user
23 | *.userosscache
24 | *.sln.docstates
25 | *.cache
26 | .angular
27 | .env
28 | .venv
29 | no_commit
30 | appsettings.json
31 |
32 | Tags/
33 | tmp/
34 | deploy/
35 | patoche/
36 |
37 | # User-specific files (MonoDevelop/Xamarin Studio)
38 | *.userprefs
39 |
40 | # Build results
41 | [Dd]ebug/
42 | [Dd]ebugPublic/
43 | [Rr]elease/
44 | [Rr]eleases/
45 | x64/
46 | x86/
47 | bld/
48 | [Bb]in/
49 | [Oo]bj/
50 | [Ll]og/
51 | dist/
52 |
53 | # Visual Studio 2015 cache/options directory
54 | .vs/
55 | # Uncomment if you have tasks that create the project's static files in wwwroot
56 | #wwwroot/
57 |
58 | # MSTest test Results
59 | [Tt]est[Rr]esult*/
60 | [Bb]uild[Ll]og.*
61 |
62 | # NUNIT
63 | *.VisualState.xml
64 | TestResult.xml
65 |
66 | # Build Results of an ATL Project
67 | [Dd]ebugPS/
68 | [Rr]eleasePS/
69 | dlldata.c
70 |
71 | # .NET Core
72 | project.lock.json
73 | project.fragment.lock.json
74 | artifacts/
75 | **/Properties/launchSettings.json
76 |
77 | *_i.c
78 | *_p.c
79 | *_i.h
80 | *.ilk
81 | *.meta
82 | *.obj
83 | *.pch
84 | *.pdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *.log
95 | *.vspscc
96 | *.vssscc
97 | .builds
98 | *.pidb
99 | *.svclog
100 | *.scc
101 |
102 | # Chutzpah Test files
103 | _Chutzpah*
104 |
105 | # Visual C++ cache files
106 | ipch/
107 | *.aps
108 | *.ncb
109 | *.opendb
110 | *.opensdf
111 | *.sdf
112 | *.cachefile
113 | *.VC.db
114 | *.VC.VC.opendb
115 |
116 | # Visual Studio profiler
117 | *.psess
118 | *.vsp
119 | *.vspx
120 | *.sap
121 |
122 | # TFS 2012 Local Workspace
123 | $tf/
124 |
125 | # Guidance Automation Toolkit
126 | *.gpState
127 |
128 | # ReSharper is a .NET coding add-in
129 | _ReSharper*/
130 | *.[Rr]e[Ss]harper
131 | *.DotSettings.user
132 |
133 | # JustCode is a .NET coding add-in
134 | .JustCode
135 |
136 | # TeamCity is a build add-in
137 | _TeamCity*
138 |
139 | # DotCover is a Code Coverage Tool
140 | *.dotCover
141 |
142 | # Visual Studio code coverage results
143 | *.coverage
144 | *.coveragexml
145 |
146 | # NCrunch
147 | _NCrunch_*
148 | .*crunch*.local.xml
149 | nCrunchTemp_*
150 |
151 | # MightyMoose
152 | *.mm.*
153 | AutoTest.Net/
154 |
155 | # Web workbench (sass)
156 | .sass-cache/
157 |
158 | # Installshield output folder
159 | [Ee]xpress/
160 |
161 | # DocProject is a documentation generator add-in
162 | DocProject/buildhelp/
163 | DocProject/Help/*.HxT
164 | DocProject/Help/*.HxC
165 | DocProject/Help/*.hhc
166 | DocProject/Help/*.hhk
167 | DocProject/Help/*.hhp
168 | DocProject/Help/Html2
169 | DocProject/Help/html
170 |
171 | # Click-Once directory
172 | publish/
173 |
174 | # Publish Web Output
175 | *.[Pp]ublish.xml
176 | *.azurePubxml
177 | # TODO: Uncomment the next line to ignore your web deploy settings.
178 | # By default, sensitive information, such as encrypted password
179 | # should be stored in the .pubxml.user file.
180 | #*.pubxml
181 | *.pubxml.user
182 | *.publishproj
183 |
184 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
185 | # checkin your Azure Web App publish settings, but sensitive information contained
186 | # in these scripts will be unencrypted
187 | PublishScripts/
188 |
189 | # NuGet Packages
190 | *.nupkg
191 | # The packages folder can be ignored because of Package Restore
192 | **/packages/*
193 | # except build/, which is used as an MSBuild target.
194 | !**/packages/build/
195 | # Uncomment if necessary however generally it will be regenerated when needed
196 | #!**/packages/repositories.config
197 | # NuGet v3's project.json files produces more ignorable files
198 | *.nuget.props
199 | *.nuget.targets
200 |
201 | # Microsoft Azure Build Output
202 | csx/
203 | *.build.csdef
204 |
205 | # Microsoft Azure Emulator
206 | ecf/
207 | rcf/
208 |
209 | # Windows Store app package directories and files
210 | AppPackages/
211 | BundleArtifacts/
212 | Package.StoreAssociation.xml
213 | _pkginfo.txt
214 |
215 | # Visual Studio cache files
216 | # files ending in .cache can be ignored
217 | *.[Cc]ache
218 | # but keep track of directories ending in .cache
219 | !*.[Cc]ache/
220 |
221 | # Others
222 | ClientBin/
223 | ~$*
224 | *~
225 | *.dbmdl
226 | *.dbproj.schemaview
227 | *.jfm
228 | *.pfx
229 | *.publishsettings
230 | orleans.codegen.cs
231 |
232 | # Since there are multiple workflows, uncomment next line to ignore bower_components
233 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
234 | #bower_components/
235 |
236 | # RIA/Silverlight projects
237 | Generated_Code/
238 |
239 | # Backup & report files from converting an old project file
240 | # to a newer Visual Studio version. Backup files are not needed,
241 | # because we have git ;-)
242 | _UpgradeReport_Files/
243 | Backup*/
244 | UpgradeLog*.XML
245 | UpgradeLog*.htm
246 |
247 | # SQL Server files
248 | *.mdf
249 | *.ldf
250 | *.ndf
251 |
252 | # Business Intelligence projects
253 | *.rdl.data
254 | *.bim.layout
255 | *.bim_*.settings
256 |
257 | # Microsoft Fakes
258 | FakesAssemblies/
259 |
260 | # GhostDoc plugin setting file
261 | *.GhostDoc.xml
262 |
263 | # Node.js Tools for Visual Studio
264 | .ntvs_analysis.dat
265 | node_modules/
266 |
267 | # Typescript v1 declaration files
268 | typings/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush
299 | .cr/
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Telerik's JustMock configuration file
310 | *.jmconfig
311 |
312 | # BizTalk build output
313 | *.btp.cs
314 | *.btm.cs
315 | *.odx.cs
316 | *.xsd.cs
317 |
318 | ### VisualStudio Patch ###
319 | # By default, sensitive information, such as encrypted password
320 | # should be stored in the .pubxml.user file.
321 |
322 |
323 | # End of https://www.gitignore.io/api/visualstudio
324 | TestsWeb/debug.log
325 |
326 | venv.ps1
327 | *.code-workspace
328 | run.cmd
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Azure Cosmos DB MCP CLient & Server
2 |
3 | This repository contains a project that shows how to create an MCP Server and client for Azure Cosmos DB. The project is divided into 2 parts:
4 |
5 | - Frontend application: NextJS 15 application that displays a products catalog and features an AI Assistant that helps users to find products in the catalog and get past orders
6 | - an MCP Server component, connected to the Azure Cosmos DB NoSQL database and responsible for reading products and orders from the database.
7 |
8 |
9 | 
10 |
11 | ## Azure Architecture
12 |
13 | - an Azure Cosmos DB NoSQL database that stores the product catalog
14 | - a node.js server that serves as the MCP Server component
15 |
16 |
17 | ## References
18 |
19 | - [Create an Azure Cosmos DB for NoSQL account](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/quickstart-portal)
20 | - [Create an Azure Storage account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal)
21 | - [Create a Windows virtual machine](https://learn.microsoft.com/en-us/azure/virtual-machines/windows/quick-create-portal)
22 | - [Windows execution policies](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.5)
23 |
24 | ## Step-by-step walkthrough
25 |
26 | ### Installation
27 |
28 | **Azure Cosmos DB**
29 |
30 | In the Azure portal, create an Azure Cosmos DB for NoSQL account.
31 |
32 | - Give a unique name for your Azure Cosmos DB account. We will be using cosmos-eastus2-nosql-2 in the rest of this walkthrough.
33 |
34 | 
35 |
36 | - Click on "Next: Global distribution"
37 |
38 | 
39 |
40 | - Accept the default values and click on "Next: Networking"
41 |
42 | 
43 |
44 | - Accept the default values and click on "Next: Backup Policy"
45 | - Select "Periodic" backup policy
46 | - Select "Locally-redundant backup storage"
47 |
48 | 
49 |
50 | - Click on "Next: Encryption"
51 |
52 | 
53 |
54 | - Click on "Review and Create" to start validation
55 |
56 | 
57 |
58 | - Click on "Create" to start the creation of the Azure Cosmos DB for NoSQL account
59 |
60 | For this project, you will need to enable vector support on the Azure Cosmos DB account.
61 | - In the settings section, select Features, then "Vector Search for NoSQL API"
62 |
63 | - In the panel that opens, click on the Enable button
64 |
65 | 
66 |
67 | - Create the Azure Cosmos DB eShop database and the Products container
68 |
69 | - Click on "..." next to eShop to display the contextual menu and select "New container" to create the "carts" container in the eShop database.
70 |
71 | Make sure that the partition key is **_"/id"_** (the partition key is case-sensitive)
72 |
73 | Expand "Container Vector Policy" and click on the "Add vector embedding" button
74 |
75 | 
76 |
77 | - Create the carts container
78 |
79 | 
80 |
81 | **Storage account**
82 |
83 | 1. Create a storage account to store the product images
84 |
85 | For more details, refer to the documentation: https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal
86 |
87 | 
88 |
89 | 
90 |
91 | 
92 |
93 | 
94 |
95 | 
96 |
97 | 
98 |
99 |
100 | **Install software pre-requisites **
101 |
102 | 1. Create a virtual machine in Azure or use your local computer
103 | 2. Install node.js v22.13.1 (LTS) from https://nodejs.org/en/download
104 | 3. Install Visual Studio Code x64 1.97.0 from https://code.visualstudio.com/download
105 | 4. Install Git 2.47.12 x64 from https://git-scm.com/downloads
106 | 5. Install .NET SDK x64 v9.0.102 from https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-9.0.102-windows-x64-installer
107 | 6. Open a terminal window and add nuget source with
108 |
109 | ```sh
110 | dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
111 | ```
112 |
113 | 7. If necessary, change PowerShell execution policies for Windows computers. Open a Powershell window **in administrator mode** and run this command
114 |
115 | ```sh
116 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
117 | ```
118 |
119 | 8. If necessary, install nuget, powershell, az cli and az modules
120 |
121 | ```sh
122 | # install az cli
123 | winget install -e --id Microsoft.AzureCLI
124 |
125 | # install nuget and reference nuget source
126 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
127 |
128 | # update to latest Powershell release (7.5 as of writing)
129 | winget install --id Microsoft.PowerShell --source winget
130 |
131 | # install az modules
132 | Install-Module -Name Az -Repository PSGallery -Force -AllowClobber
133 | ```
134 |
135 | 9. Open a terminal window and clone the repository:
136 |
137 | ```sh
138 | git clone https://github.com/patrice-truong/cosmosdb-mcp.git
139 | cd cosmosdb-mcp
140 | ```
141 |
142 | 10. Navigate to the nextjs folder and install dependencies
143 |
144 | ```sh
145 | cd cosmosdb-mcp/nextjs
146 | npm install --legacy-peer-deps
147 | ```
148 |
149 | 11. In the nextjs folder, create and configure an .env file with the following values:
150 |
151 | ```sh
152 | AZURE_COSMOSDB_NOSQL_ENDPOINT=https://<cosmosdb_account_name>.documents.azure.com:443/
153 | AZURE_COSMOSDB_NOSQL_DATABASE=eshop
154 | AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER=products
155 | AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER=carts
156 | AZURE_COSMOSDB_NOSQL_ORDERS_CONTAINER=orders
157 | AZURE_STORAGE_ACCOUNT_NAME=<storage_account_name>
158 | AZURE_STORAGE_CONTAINER_NAME=<container_name>
159 | ```
160 |
161 | 12. Get your tenant ID. The tenant ID can be retrieved with this command:
162 |
163 | ```sh
164 | az login
165 | az account show --query tenantId -o tsv
166 | ```
167 |
168 | 13. In the webapi folder, configure the appsettings.json file and replace the tenant_id with the value obtained in the previous step:
169 |
170 | ```sh
171 | {
172 | "CosmosDb": {
173 | "Endpoint": "https:/<cosmosdb_account_name>.documents.azure.com:443/",
174 | "TenantId": "<tenant_id>",
175 | "DatabaseName": "eshop",
176 | "ProductsContainerName": "products",
177 | "CartsContainerName": "carts",
178 | "OrdersContainerName": "orders"
179 | },
180 | "AzureBlobStorage": {
181 | "AccountName": "<storage_account_name>"
182 | }
183 | }
184 | ```
185 | 14. Create an app registration in the Azure Portal
186 | 15. Create an app secret in the Azure Portal
187 | 16. You will need to allow your app to get access to Azure Cosmos DB. Retrieve the 4 ids mentioned below and modify the file "populate/set_rbac.ps1".
188 |
189 | | Variable | Reference |
190 | | ---------------------------- | ------------------------------------------------- |
191 | | Subscription Id | Cosmos DB > Overview > Subscription Id |
192 | | Azure Cosmos DB account name | cosmos-eastus2-nosql-2 |
193 | | Resource group name | Cosmos DB > Overview > Resource group name |
194 | | Principal Id | App registration Object Id |
195 |
196 | ```sh
197 | $SubscriptionId = "<subscription-id>" # Azure subscription id
198 | $AccountName = "<cosmosdb-account-name>" # cosmos db account name
199 | $ResourceGroupName = "<resource-group-name>" # resource group name of the Cosmos DB account
200 | $PrincipalId = "<principal-id>" # object id of the app registered in Entra ID
201 | ```
202 |
203 | 17. Open a Powershell prompt, run Connect-AzAccount and execute ./set_rbac.ps1
204 |
205 | 
206 |
207 | 18. Allow your app (or virtual machine) to access the storage account
208 |
209 | - In the Azure portal, goto your storage account
210 | - Select Access Control (IAM) in the menu
211 |
212 | 
213 |
214 | - Click on "Add role assignment"
215 | - In the filter textbox, type "Storage Blob Data Contributor"
216 |
217 | 
218 |
219 | - Click on "Members"
220 | - Select the name of your application
221 |
222 | 
223 |
224 | - Click on the "Select" button
225 | - Click on "Review and assign"
226 |
227 | 
228 |
229 | 19. Create a container and copy the content of the "azure-storage" folder to your storage account
230 |
231 | 
232 |
233 | 
234 |
235 | 
236 |
237 | 20. Build webapi backend project with dotnet build
238 |
239 | ```sh
240 | cd webapi
241 | dotnet build
242 | ```
243 |
244 |  18. On your secondary region VM (Australia East), modify the .env file with the IP address of the socket server in your primary region (East US 2)
245 |
246 | 
247 |
248 | 19. There is no authentication built into this project. The user email is hard-coded in /nextjs/models/constants.ts. Change it to suit your demo needs
249 |
250 | 
251 |
252 | 19. In mcp-server and nextjs folders, copy .env.template to .env and modify the values to suit your demo needs
253 |
254 | ```json
255 | AZURE_COSMOSDB_NOSQL_ENDPOINT=https://<cosmosdb_account>.documents.azure.com:443/
256 | AZURE_COSMOSDB_NOSQL_DATABASE=eshop
257 | AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER=products
258 | AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER=carts
259 | AZURE_COSMOSDB_NOSQL_ORDERS_CONTAINER=orders
260 |
261 | NEXT_PUBLIC_AZURE_TENANT_ID=<tenant_id>
262 | NEXT_PUBLIC_AZURE_CLIENT_ID=<client_id>
263 | NEXT_PUBLIC_AZURE_CLIENT_SECRET=<client_secret>
264 |
265 | NEXT_PUBLIC_AZURE_STORAGE_ACCOUNT_NAME=<storage_account_name>
266 | NEXT_PUBLIC_AZURE_STORAGE_CONTAINER_NAME=img
267 |
268 | AZURE_OPENAI_ENDPOINT=https://<azure_openai_account>.openai.azure.com/
269 | AZURE_OPENAI_API_KEY=<azure_openai_key>
270 | AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-3-small
271 | AZURE_OPENAI_API_VERSION=2024-05-01-preview
272 | ```
273 |
274 | 20. Build nextjs frontend project
275 |
276 | ```sh
277 | cd nextjs
278 | npm run build
279 | ```
280 |
281 | 
282 |
283 | ## Populate the products catalog
284 |
285 | In this section, we'll read the products catalog from the populate/catalog.json file and populate the Azure Cosmos DB for NoSQL database
286 |
287 | 1. Modify appsettings.json with your cosmosdb account name and
288 |
289 | ```json
290 | {
291 | "CosmosDb": {
292 | "Endpoint": "https://<cosmosdb_account_name>.documents.azure.com:443/",
293 | "TenantId": "<tenant_id>",
294 | "DatabaseName": "eshop",
295 | "ProductsContainerName": "products",
296 | "OrdersContainerName": "orders",
297 | }
298 | }
299 | ```
300 |
301 | 2. Open a terminal window, navigate to the populate folder, execute az login, then dotnet run
302 |
303 | 
304 |
305 | 3. Verify that the Azure Cosmos DB container has been properly populated
306 |
307 | 
308 |
309 | ## Demo script
310 |
311 | **Demo initialization:**
312 |
313 | 3. On your development computer, start the mcp server
314 |
315 | ```sh
316 | cd mcp-server
317 | npx ts-node src/server.ts
318 | ```
319 |
320 | 3. Start the front end project
321 |
322 | - NextJS front end (store front)
323 | - cd nextjs
324 | - npm start
325 |
326 | 4. Optionally, open a command prompt and start the MCP inspector with this command:
327 | npx -y @modelcontextprotocol/inspector
328 |
329 |
330 | **Demo steps:**
331 |
332 | 1. Navigate to http://localhost:3002.
333 | 2.
334 | 2. Click on AI Assistant icon in the top right corner
335 | 3. Enter "I'm interested in backpacks" (the list of product refreshes with a list of backpacks)
336 | 4. Enter "Get my orders" (the list of orders refreshes with a list of orders)
337 |
338 |
339 | 
340 |
```
--------------------------------------------------------------------------------
/mcp-server/run_mcp_server.cmd:
--------------------------------------------------------------------------------
```
1 | npx ts-node src/server.ts
```
--------------------------------------------------------------------------------
/nextjs/models/constants.ts:
--------------------------------------------------------------------------------
```typescript
1 | // /models/constants.ts
2 | export const EMAIL = '[email protected]'
3 |
4 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "dependencies": {
3 | "@types/uuid": "^10.0.0",
4 | "uuid": "^11.1.0"
5 | }
6 | }
7 |
```
--------------------------------------------------------------------------------
/nextjs/postcss.config.js:
--------------------------------------------------------------------------------
```javascript
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
```
--------------------------------------------------------------------------------
/nextjs/models/cart.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CartItem } from "./cartItem"
2 |
3 | export type Cart = {
4 | userName: string
5 | items: CartItem[]
6 | }
7 |
```
--------------------------------------------------------------------------------
/nextjs/models/cartItem.ts:
--------------------------------------------------------------------------------
```typescript
1 | export type CartItem = {
2 | id: number
3 | name: string
4 | price: number
5 | quantity: number,
6 | imageUrl: string
7 | }
```
--------------------------------------------------------------------------------
/nextjs/app/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { redirect } from "next/navigation";
2 |
3 | export default function Home() {
4 | redirect("http://localhost:3002/products");
5 | }
6 |
```
--------------------------------------------------------------------------------
/nextjs/models/product.ts:
--------------------------------------------------------------------------------
```typescript
1 | export type Product = {
2 | id: string;
3 | type: string;
4 | brand: string;
5 | description: string;
6 | name: string;
7 | price: number;
8 | imageUrl: string;
9 | };
10 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root;
6 |
7 | export { AspectRatio };
8 |
```
--------------------------------------------------------------------------------
/nextjs/lib/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { clsx, type ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
```
--------------------------------------------------------------------------------
/nextjs/app/cart/layout.tsx:
--------------------------------------------------------------------------------
```typescript
1 | export const dynamic = 'force-dynamic'
2 | export const revalidate = 0
3 |
4 | export default function CartLayout({
5 | children,
6 | }: {
7 | children: React.ReactNode
8 | }) {
9 | return children
10 | }
```
--------------------------------------------------------------------------------
/nextjs/next-env.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | /// <reference types="next" />
2 | /// <reference types="next/image-types/global" />
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
```
--------------------------------------------------------------------------------
/nextjs/models/cartContextType.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CartItem } from "./cartItem"
2 |
3 | export type CartContextType = {
4 | items: CartItem[]
5 | addItem: (item: Omit<CartItem, 'quantity'>) => void
6 | removeItem: (id: number) => void
7 | updateQuantity: (id: number, quantity: number) => void
8 | clearCart: () => void
9 | }
10 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { cn } from '@/lib/utils';
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes<HTMLDivElement>) {
7 | return (
8 | <div
9 | className={cn('animate-pulse rounded-md bg-muted', className)}
10 | {...props}
11 | />
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
```
--------------------------------------------------------------------------------
/populate/product.cs:
--------------------------------------------------------------------------------
```csharp
1 | public class Product
2 | {
3 | public int id { get; set; }
4 | public string type { get; set; }
5 | public string brand { get; set; }
6 | public string name { get; set; }
7 | public string description { get; set; }
8 | public decimal price { get; set; }
9 | public float[] embedding { get; set;}
10 | }
11 |
```
--------------------------------------------------------------------------------
/mcp-server/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "outDir": "./dist",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": ["node_modules"]
14 | }
```
--------------------------------------------------------------------------------
/nextjs/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
4 |
5 | const Collapsible = CollapsiblePrimitive.Root;
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent };
12 |
```
--------------------------------------------------------------------------------
/nextjs/next.config.js:
--------------------------------------------------------------------------------
```javascript
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | env: {
4 | AZURE_STORAGE_ACCOUNT_NAME: process.env.AZURE_STORAGE_ACCOUNT_NAME,
5 | AZURE_STORAGE_CONTAINER_NAME: process.env.AZURE_STORAGE_CONTAINER_NAME,
6 | },
7 | // output: 'export',
8 | eslint: {
9 | ignoreDuringBuilds: true,
10 | },
11 | images: { unoptimized: true },
12 | };
13 |
14 | module.exports = nextConfig;
15 |
```
--------------------------------------------------------------------------------
/nextjs/components.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
21 |
```
--------------------------------------------------------------------------------
/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
```
--------------------------------------------------------------------------------
/mcp-server/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-server",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "start": "node dist/server.js",
7 | "dev": "nodemon src/server.ts",
8 | "build": "tsc"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "description": "",
14 | "dependencies": {
15 | "@azure/cosmos": "^4.2.0",
16 | "@azure/identity": "^4.8.0",
17 | "@azure/openai": "^2.0.0",
18 | "@modelcontextprotocol/sdk": "^1.8.0",
19 | "@types/cors": "^2.8.17",
20 | "@types/express": "^5.0.1",
21 | "@types/node": "^22.13.13",
22 | "cors": "^2.8.5",
23 | "dotenv": "^16.4.7",
24 | "express": "^4.21.2",
25 | "nodemon": "^3.1.9",
26 | "openai": "^4.90.0",
27 | "ts-node": "^10.9.2",
28 | "typescript": "^5.8.2"
29 | }
30 | }
31 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/label.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef<typeof LabelPrimitive.Root>,
15 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
16 | VariantProps<typeof labelVariants>
17 | >(({ className, ...props }, ref) => (
18 | <LabelPrimitive.Root
19 | ref={ref}
20 | className={cn(labelVariants(), className)}
21 | {...props}
22 | />
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
```
--------------------------------------------------------------------------------
/nextjs/components/CartItemCount.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client'
2 |
3 | import Link from 'next/link'
4 | import { ShoppingCart } from 'lucide-react'
5 | import io from 'socket.io-client'
6 | import { useCart } from '@/context/CartContext'
7 | import { useEffect } from 'react'
8 |
9 | export default function CartItemCount () {
10 | const { items } = useCart()
11 | const itemCount = items.reduce((total, item) => total + item.quantity, 0)
12 |
13 | return (
14 | <Link href='/cart' className='relative p-2 hover:bg-gray-100 rounded-full'>
15 | <ShoppingCart className='h-6 w-6' />
16 | {itemCount > 0 && (
17 | <span className='absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-100 bg-red-600 rounded-full'>
18 | {itemCount}
19 | </span>
20 | )}
21 | </Link>
22 | )
23 | }
24 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/separator.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef<typeof SeparatorPrimitive.Root>,
10 | React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 | <SeparatorPrimitive.Root
17 | ref={ref}
18 | decorative={decorative}
19 | orientation={orientation}
20 | className={cn(
21 | 'shrink-0 bg-border',
22 | orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
23 | className
24 | )}
25 | {...props}
26 | />
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
7 |
8 | const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
9 | ({ className, ...props }, ref) => {
10 | return (
11 | <textarea
12 | className={cn(
13 | 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
14 | className
15 | )}
16 | ref={ref}
17 | {...props}
18 | />
19 | );
20 | }
21 | );
22 | Textarea.displayName = 'Textarea';
23 |
24 | export { Textarea };
25 |
```
--------------------------------------------------------------------------------
/nextjs/app/api/orders/route.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { cosmosDBService } from '@/lib/cosmosdb';
3 |
4 | export async function POST(request: NextRequest) {
5 | try {
6 | const order = await request.json();
7 |
8 | if (!order || !order.items || !order.total) {
9 | return NextResponse.json(
10 | { error: 'Invalid order data' },
11 | { status: 400 }
12 | );
13 | }
14 |
15 | const { resources: items } = await cosmosDBService.database
16 | .container('orders')
17 | .items.create(order);
18 |
19 | return NextResponse.json({
20 | message: 'Order created successfully',
21 | orderId: order.id
22 | });
23 |
24 | } catch (error) {
25 | console.error('Error creating order:', error);
26 | return NextResponse.json(
27 | { error: 'Failed to create order' },
28 | { status: 500 }
29 | );
30 | }
31 | }
```
--------------------------------------------------------------------------------
/nextjs/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import { useToast } from '@/hooks/use-toast';
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from '@/components/ui/toast';
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 | <ToastProvider>
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 | <Toast key={id} {...props}>
21 | <div className="grid gap-1">
22 | {title && <ToastTitle>{title}</ToastTitle>}
23 | {description && (
24 | <ToastDescription>{description}</ToastDescription>
25 | )}
26 | </div>
27 | {action}
28 | <ToastClose />
29 | </Toast>
30 | );
31 | })}
32 | <ToastViewport />
33 | </ToastProvider>
34 | );
35 | }
36 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/progress.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as ProgressPrimitive from '@radix-ui/react-progress';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef<typeof ProgressPrimitive.Root>,
10 | React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
11 | >(({ className, value, ...props }, ref) => (
12 | <ProgressPrimitive.Root
13 | ref={ref}
14 | className={cn(
15 | 'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
16 | className
17 | )}
18 | {...props}
19 | >
20 | <ProgressPrimitive.Indicator
21 | className="h-full w-full flex-1 bg-primary transition-all"
22 | style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
23 | />
24 | </ProgressPrimitive.Root>
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
```
--------------------------------------------------------------------------------
/nextjs/app/api/orders/[id]/route.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { cosmosDBService } from '@/lib/cosmosdb';
3 |
4 | export async function GET(
5 | request: NextRequest,
6 | { params }: { params: { id: string } }
7 | ) {
8 | try {
9 | const { resources: items } = await cosmosDBService.ordersContainer.items
10 | .query({
11 | query: 'SELECT * FROM c WHERE c.id = @id',
12 | parameters: [{ name: '@id', value: params.id }]
13 | })
14 | .fetchAll();
15 |
16 | if (items.length === 0) {
17 | return NextResponse.json({ error: 'Order not found' }, { status: 404 });
18 | }
19 |
20 | return NextResponse.json({
21 | data: items[0],
22 | statusCode: 200
23 | });
24 | } catch (error) {
25 | console.error('Error fetching order:', error);
26 | return NextResponse.json(
27 | { error: 'Failed to fetch order' },
28 | { status: 500 }
29 | );
30 | }
31 | }
```
--------------------------------------------------------------------------------
/nextjs/components/ui/input.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes<HTMLInputElement> {}
7 |
8 | const Input = React.forwardRef<HTMLInputElement, InputProps>(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 | <input
12 | type={type}
13 | className={cn(
14 | 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
15 | className
16 | )}
17 | ref={ref}
18 | {...props}
19 | />
20 | );
21 | }
22 | );
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import { useTheme } from 'next-themes';
4 | import { Toaster as Sonner } from 'sonner';
5 |
6 | type ToasterProps = React.ComponentProps<typeof Sonner>;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = 'system' } = useTheme();
10 |
11 | return (
12 | <Sonner
13 | theme={theme as ToasterProps['theme']}
14 | className="toaster group"
15 | toastOptions={{
16 | classNames: {
17 | toast:
18 | 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
19 | description: 'group-[.toast]:text-muted-foreground',
20 | actionButton:
21 | 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
22 | cancelButton:
23 | 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
24 | },
25 | }}
26 | {...props}
27 | />
28 | );
29 | };
30 |
31 | export { Toaster };
32 |
```
--------------------------------------------------------------------------------
/mcp-server/src/aoai.ts:
--------------------------------------------------------------------------------
```typescript
1 | import * as dotenv from "dotenv";
2 |
3 | import { AzureOpenAI } from "openai";
4 |
5 | const FILE_NAME = "src/aoai.ts";
6 |
7 | /**
8 | * Get embeddings from Azure OpenAI
9 | * @param {string} q
10 | * @returns {Promise<number[]>}
11 | */
12 | export const getEmbeddingsAsync = async (q: string) => {
13 | try {
14 | console.log(`[${FILE_NAME}] q: ${q}`);
15 |
16 | dotenv.config();
17 | console.log(`[${FILE_NAME}] Embedding model=${process.env.AZURE_OPENAI_EMBEDDING_MODEL!}`);
18 |
19 | const client = new AzureOpenAI({
20 | endpoint: process.env.AZURE_OPENAI_ENDPOINT,
21 | apiKey: process.env.AZURE_OPENAI_API_KEY!,
22 | apiVersion: "2024-12-01-preview",
23 | });
24 |
25 | const embeddingResponse = await client.embeddings.create({
26 | model: process.env.AZURE_OPENAI_EMBEDDING_MODEL!,
27 | input: q,
28 | });
29 | const [{ embedding }] = embeddingResponse.data;
30 | // console.log(`[${FILE_NAME}] Embeddings: ${JSON.stringify(embedding.slice(0, 50))}...`)
31 | return embedding;
32 | } catch (error) {
33 | console.error(`[${FILE_NAME}] Error getting embeddings: ${JSON.stringify(error)}`);
34 | }
35 | };
36 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
5 | import { Check } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef<typeof CheckboxPrimitive.Root>,
11 | React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12 | >(({ className, ...props }, ref) => (
13 | <CheckboxPrimitive.Root
14 | ref={ref}
15 | className={cn(
16 | 'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
17 | className
18 | )}
19 | {...props}
20 | >
21 | <CheckboxPrimitive.Indicator
22 | className={cn('flex items-center justify-center text-current')}
23 | >
24 | <Check className="h-4 w-4" />
25 | </CheckboxPrimitive.Indicator>
26 | </CheckboxPrimitive.Root>
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/slider.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SliderPrimitive from '@radix-ui/react-slider';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef<typeof SliderPrimitive.Root>,
10 | React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
11 | >(({ className, ...props }, ref) => (
12 | <SliderPrimitive.Root
13 | ref={ref}
14 | className={cn(
15 | 'relative flex w-full touch-none select-none items-center',
16 | className
17 | )}
18 | {...props}
19 | >
20 | <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
21 | <SliderPrimitive.Range className="absolute h-full bg-primary" />
22 | </SliderPrimitive.Track>
23 | <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
24 | </SliderPrimitive.Root>
25 | ));
26 | Slider.displayName = SliderPrimitive.Root.displayName;
27 |
28 | export { Slider };
29 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/badge.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes<HTMLDivElement>,
28 | VariantProps<typeof badgeVariants> {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 | <div className={cn(badgeVariants({ variant }), className)} {...props} />
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/switch.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SwitchPrimitives from '@radix-ui/react-switch';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef<typeof SwitchPrimitives.Root>,
10 | React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
11 | >(({ className, ...props }, ref) => (
12 | <SwitchPrimitives.Root
13 | className={cn(
14 | 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
15 | className
16 | )}
17 | {...props}
18 | ref={ref}
19 | >
20 | <SwitchPrimitives.Thumb
21 | className={cn(
22 | 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
23 | )}
24 | />
25 | </SwitchPrimitives.Root>
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef<typeof TooltipPrimitive.Content>,
16 | React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 | <TooltipPrimitive.Content
19 | ref={ref}
20 | sideOffset={sideOffset}
21 | className={cn(
22 | 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
23 | className
24 | )}
25 | {...props}
26 | />
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
```
--------------------------------------------------------------------------------
/mcp-server/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import 'dotenv/config'
2 |
3 | import express, { Request, Response } from 'express'
4 |
5 | import { CosmosDBMcpServer } from './cosmosdb-mcp-server'
6 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
7 | import cors from 'cors'
8 |
9 | const app = express()
10 |
11 | // Add CORS middleware
12 | app.use(cors({
13 | origin: 'http://localhost:3002',
14 | credentials: true,
15 | methods: ['GET', 'POST'],
16 | allowedHeaders: ['Content-Type']
17 | }))
18 |
19 | const cosmosDBMcpServer = new CosmosDBMcpServer()
20 | const server = cosmosDBMcpServer.getServer()
21 |
22 | const transports: { [sessionId: string]: SSEServerTransport } = {}
23 |
24 | app.get('/sse', async (_: Request, res: Response) => {
25 | const transport = new SSEServerTransport('/messages', res)
26 | transports[transport.sessionId] = transport
27 | res.on('close', () => {
28 | delete transports[transport.sessionId]
29 | })
30 | await server.connect(transport)
31 | })
32 |
33 | app.post('/messages', async (req: Request, res: Response) => {
34 | const sessionId = req.query.sessionId as string
35 | const transport = transports[sessionId]
36 | if (transport) {
37 | await transport.handlePostMessage(req, res)
38 | } else {
39 | res.status(400).send('No transport found for sessionId')
40 | }
41 | })
42 |
43 | app.listen(3001)
44 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef<typeof HoverCardPrimitive.Content>,
14 | React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 | <HoverCardPrimitive.Content
17 | ref={ref}
18 | align={align}
19 | sideOffset={sideOffset}
20 | className={cn(
21 | 'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
22 | className
23 | )}
24 | {...props}
25 | />
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/popover.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as PopoverPrimitive from '@radix-ui/react-popover';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef<typeof PopoverPrimitive.Content>,
14 | React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 | <PopoverPrimitive.Portal>
17 | <PopoverPrimitive.Content
18 | ref={ref}
19 | align={align}
20 | sideOffset={sideOffset}
21 | className={cn(
22 | 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
23 | className
24 | )}
25 | {...props}
26 | />
27 | </PopoverPrimitive.Portal>
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
```
--------------------------------------------------------------------------------
/nextjs/lib/mcp-client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3 |
4 | export class MCPClient {
5 | private client: Client | null = null;
6 |
7 | private initialize = async () => {
8 | if (this.client) return;
9 |
10 | this.client = new Client(
11 | {
12 | name: "cosmosdb-client",
13 | version: "1.0.0"
14 | },
15 | {
16 | capabilities: {
17 | prompts: {},
18 | resources: {},
19 | tools: {}
20 | }
21 | }
22 | );
23 |
24 | const transport = new SSEClientTransport(
25 | new URL("/sse", "http://localhost:3001/"),
26 | {
27 | requestInit: {
28 | headers: {
29 | 'Content-Type': 'text/event-stream',
30 | }
31 | }
32 | }
33 | );
34 |
35 | try {
36 | await this.client.connect(transport);
37 | } catch (e) {
38 | console.error('Failed to connect to MCP server:', e);
39 | this.client = null;
40 | }
41 | };
42 |
43 | getClient = async (): Promise<Client | null> => {
44 | await this.initialize();
45 | return this.client;
46 | };
47 |
48 | static create = async (): Promise<MCPClient> => {
49 | const instance = new MCPClient();
50 | await instance.initialize();
51 | return instance;
52 | };
53 | }
54 |
55 | export const createMCPClient = async () => {
56 | const mcpClient = await MCPClient.create();
57 | return mcpClient.getClient();
58 | };
```
--------------------------------------------------------------------------------
/nextjs/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef<typeof AvatarPrimitive.Root>,
10 | React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11 | >(({ className, ...props }, ref) => (
12 | <AvatarPrimitive.Root
13 | ref={ref}
14 | className={cn(
15 | 'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
16 | className
17 | )}
18 | {...props}
19 | />
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef<typeof AvatarPrimitive.Image>,
25 | React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26 | >(({ className, ...props }, ref) => (
27 | <AvatarPrimitive.Image
28 | ref={ref}
29 | className={cn('aspect-square h-full w-full', className)}
30 | {...props}
31 | />
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef<typeof AvatarPrimitive.Fallback>,
37 | React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38 | >(({ className, ...props }, ref) => (
39 | <AvatarPrimitive.Fallback
40 | ref={ref}
41 | className={cn(
42 | 'flex h-full w-full items-center justify-center rounded-full bg-muted',
43 | className
44 | )}
45 | {...props}
46 | />
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TogglePrimitive from '@radix-ui/react-toggle';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const toggleVariants = cva(
10 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
11 | {
12 | variants: {
13 | variant: {
14 | default: 'bg-transparent',
15 | outline:
16 | 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
17 | },
18 | size: {
19 | default: 'h-10 px-3',
20 | sm: 'h-9 px-2.5',
21 | lg: 'h-11 px-5',
22 | },
23 | },
24 | defaultVariants: {
25 | variant: 'default',
26 | size: 'default',
27 | },
28 | }
29 | );
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef<typeof TogglePrimitive.Root>,
33 | React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
34 | VariantProps<typeof toggleVariants>
35 | >(({ className, variant, size, ...props }, ref) => (
36 | <TogglePrimitive.Root
37 | ref={ref}
38 | className={cn(toggleVariants({ variant, size, className }))}
39 | {...props}
40 | />
41 | ));
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName;
44 |
45 | export { Toggle, toggleVariants };
46 |
```
--------------------------------------------------------------------------------
/populate/set_rbac.ps1:
--------------------------------------------------------------------------------
```
1 | $SubscriptionId = "<subscription_id>" # Azure subscription id
2 | $AccountName = "<cosmosdb_account_name>" # cosmos db account name
3 | $ResourceGroupName = "rg-cosmosdb" # resource group name of the Cosmos DB account
4 | $PrincipalId = "481510d9-3e2b-4582-a356-ffb8bca58b71" # id of the virtual machine in Entra ID
5 |
6 | # Assign the "Cosmos DB Built-in Data Reader" role to an identity
7 | $parameters = @{
8 | ResourceGroupName = $ResourceGroupName
9 | AccountName = $AccountName
10 | RoleDefinitionId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001"
11 | PrincipalId = $PrincipalId
12 | Scope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName"
13 | }
14 | New-AzCosmosDBSqlRoleAssignment @parameters
15 |
16 | # Assign the "Cosmos DB Built-in Data Contributor" role to an identity
17 | $parameters = @{
18 | ResourceGroupName = $ResourceGroupName
19 | AccountName = $AccountName
20 | RoleDefinitionId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002"
21 | PrincipalId = $PrincipalId
22 | Scope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.DocumentDB/databaseAccounts/$AccountName"
23 | }
24 | New-AzCosmosDBSqlRoleAssignment @parameters
```
--------------------------------------------------------------------------------
/nextjs/app/api/products/route.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | import { SqlQuerySpec } from '@azure/cosmos';
4 | import { cosmosDBService } from '@/lib/cosmosdb';
5 |
6 | export async function GET(request: NextRequest) {
7 | const startTime = Date.now();
8 |
9 | try {
10 | // Get query parameters
11 | const searchParams = request.nextUrl.searchParams;
12 | const token = searchParams.get('token');
13 | const ids = searchParams.get('ids');
14 |
15 | let result;
16 |
17 | if (ids) {
18 | // If IDs are provided, fetch only those products
19 | const idArray = ids.split(',');
20 | const sqlQuerySpec: SqlQuerySpec = {
21 | query: 'SELECT c.id, c.type, c.brand, c.name, c.description, c.price FROM c WHERE ARRAY_CONTAINS(@ids, c.id)',
22 | parameters: [
23 | {
24 | name: '@ids',
25 | value: idArray
26 | }
27 | ]
28 | };
29 |
30 | const { resources: items } = await cosmosDBService.productsContainer.items
31 | .query(sqlQuerySpec)
32 | .fetchAll();
33 |
34 | const duration = Date.now() - startTime;
35 |
36 | result = {
37 | data: items,
38 | duration,
39 | token
40 | };
41 | } else {
42 | // Otherwise, fetch all products using the existing service method
43 | result = await cosmosDBService.getProducts();
44 | }
45 |
46 | return NextResponse.json(result);
47 |
48 | } catch (error) {
49 | console.error('Error fetching products:', error);
50 | return NextResponse.json(
51 | { error: 'Failed to fetch products' },
52 | { status: 500 }
53 | );
54 | }
55 | }
```
--------------------------------------------------------------------------------
/nextjs/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
5 | import { Circle } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef<typeof RadioGroupPrimitive.Root>,
11 | React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
12 | >(({ className, ...props }, ref) => {
13 | return (
14 | <RadioGroupPrimitive.Root
15 | className={cn('grid gap-2', className)}
16 | {...props}
17 | ref={ref}
18 | />
19 | );
20 | });
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef<typeof RadioGroupPrimitive.Item>,
25 | React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
26 | >(({ className, ...props }, ref) => {
27 | return (
28 | <RadioGroupPrimitive.Item
29 | ref={ref}
30 | className={cn(
31 | 'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
32 | className
33 | )}
34 | {...props}
35 | >
36 | <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
37 | <Circle className="h-2.5 w-2.5 fill-current text-current" />
38 | </RadioGroupPrimitive.Indicator>
39 | </RadioGroupPrimitive.Item>
40 | );
41 | });
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
43 |
44 | export { RadioGroup, RadioGroupItem };
45 |
```
--------------------------------------------------------------------------------
/nextjs/app/products/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import { Button } from "@/components/ui/button";
2 | import Image from "next/image";
3 | import Products from "@/components/Products";
4 |
5 | type Props = {
6 | searchParams: Promise<{
7 | ids?: string;
8 | }>;
9 | };
10 |
11 | export default async function ProductsPage({ searchParams }: Props) {
12 | // Wait for searchParams to resolve
13 | const params = await searchParams;
14 | const productIds = params?.ids?.split(",").filter(Boolean);
15 |
16 | return (
17 | <div className="min-h-screen bg-gray-50">
18 | <div className="relative">
19 | <div className="absolute inset-0">
20 | <Image src="https://images.unsplash.com/photo-1551698618-1dfe5d97d256?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80" alt="Winter mountain landscape" fill priority className="object-cover" />
21 | <div className="absolute inset-0 bg-gray-900/40" />
22 | </div>
23 | <div className="relative max-w-7xl mx-auto px-4 py-32 sm:px-6 lg:px-8">
24 | <h1 className="text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">Ready for a new adventure?</h1>
25 | <p className="mt-6 text-xl text-white max-w-3xl">Start the season with the latest in clothing and equipment.</p>
26 | <div className="mt-10">
27 | <Button size="lg" className="bg-white text-gray-900 hover:bg-gray-100">
28 | Shop Now
29 | </Button>
30 | </div>
31 | </div>
32 | </div>
33 |
34 | <div className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
35 | <Products productIds={productIds} />
36 | </div>
37 | </div>
38 | );
39 | }
40 |
```
--------------------------------------------------------------------------------
/nextjs/app/api/chat/route.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { AzureOpenAI } from 'openai'
2 | import { EMAIL } from '@/models/constants'
3 | import { NextResponse } from 'next/server'
4 |
5 | function validateEnvironmentVariables () {
6 | const required = ['AZURE_OPENAI_ENDPOINT', 'AZURE_OPENAI_API_KEY']
7 |
8 | for (const variable of required) {
9 | if (!process.env[variable]) {
10 | throw new Error(`Missing required environment variable: ${variable}`)
11 | }
12 | }
13 | }
14 |
15 | export async function POST (req: Request) {
16 | try {
17 | validateEnvironmentVariables()
18 | const { message, conversationHistory, tools } = await req.json()
19 |
20 | const messages = [
21 | {
22 | role: 'system',
23 | content:
24 | 'You are a helpful shopping assistant that helps customers find products and answers questions about them.'
25 | },
26 | ...conversationHistory.map((msg: any) => ({
27 | role: msg.role,
28 | content: msg.content
29 | })),
30 | { role: 'user', content: message }
31 | ]
32 |
33 | const client = new AzureOpenAI({
34 | endpoint: process.env.AZURE_OPENAI_ENDPOINT,
35 | apiKey: process.env.AZURE_OPENAI_API_KEY!,
36 | apiVersion: process.env.AZURE_OPENAI_API_VERSION!
37 | })
38 |
39 | const response = await client.chat.completions.create({
40 | messages: messages,
41 | model: process.env.AZURE_OPENAI_CHAT_MODEL!,
42 | tools: tools
43 | })
44 |
45 | const content = JSON.stringify(response)
46 | console.log('Chat API Response:', content)
47 |
48 | return NextResponse.json({ content })
49 | } catch (error) {
50 | console.error('Chat API Error:', error)
51 | return NextResponse.json(
52 | { error: 'Internal server error' },
53 | { status: 500 }
54 | )
55 | }
56 | }
57 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/alert.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const alertVariants = cva(
7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | destructive:
13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
14 | },
15 | },
16 | defaultVariants: {
17 | variant: 'default',
18 | },
19 | }
20 | );
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25 | >(({ className, variant, ...props }, ref) => (
26 | <div
27 | ref={ref}
28 | role="alert"
29 | className={cn(alertVariants({ variant }), className)}
30 | {...props}
31 | />
32 | ));
33 | Alert.displayName = 'Alert';
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes<HTMLHeadingElement>
38 | >(({ className, ...props }, ref) => (
39 | <h5
40 | ref={ref}
41 | className={cn('mb-1 font-medium leading-none tracking-tight', className)}
42 | {...props}
43 | />
44 | ));
45 | AlertTitle.displayName = 'AlertTitle';
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes<HTMLParagraphElement>
50 | >(({ className, ...props }, ref) => (
51 | <div
52 | ref={ref}
53 | className={cn('text-sm [&_p]:leading-relaxed', className)}
54 | {...props}
55 | />
56 | ));
57 | AlertDescription.displayName = 'AlertDescription';
58 |
59 | export { Alert, AlertTitle, AlertDescription };
60 |
```
--------------------------------------------------------------------------------
/nextjs/app/api/blob-url/route.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { BlobServiceClient } from "@azure/storage-blob";
2 | import { ClientSecretCredential } from "@azure/identity";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function GET(request: Request) {
6 | const { searchParams } = new URL(request.url);
7 | const blobName = searchParams.get("blob");
8 |
9 | if (!blobName) {
10 | return NextResponse.json({ error: "Blob name is required" }, { status: 400 });
11 | }
12 |
13 | try {
14 | const credential = new ClientSecretCredential(process.env.NEXT_PUBLIC_AZURE_TENANT_ID!, process.env.NEXT_PUBLIC_AZURE_CLIENT_ID!, process.env.NEXT_PUBLIC_AZURE_CLIENT_SECRET!);
15 |
16 | const blobServiceClient = new BlobServiceClient(`https://${process.env.NEXT_PUBLIC_AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`, credential);
17 |
18 | const containerClient = blobServiceClient.getContainerClient(process.env.NEXT_PUBLIC_AZURE_STORAGE_CONTAINER_NAME!);
19 | const blobClient = containerClient.getBlobClient(blobName);
20 |
21 | // Download blob content
22 | const downloadResponse = await blobClient.download();
23 |
24 | const chunks: Uint8Array[] = [];
25 |
26 | for await (const chunk of downloadResponse.readableStreamBody as AsyncIterable<Uint8Array>) {
27 | chunks.push(chunk);
28 | }
29 |
30 | const buffer = Buffer.concat(chunks);
31 |
32 | const base64String = `data:image/webp;base64,${buffer.toString("base64")}`;
33 |
34 | return NextResponse.json({ url: base64String });
35 | } catch (error) {
36 | console.error("Error accessing blob:", JSON.stringify(error, null, 2));
37 | return NextResponse.json(
38 | {
39 | error: "Failed to access blob",
40 | details: error instanceof Error ? error.message : "Unknown error",
41 | },
42 | { status: 500 }
43 | );
44 | }
45 | }
46 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef<typeof ScrollAreaPrimitive.Root>,
10 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
11 | >(({ className, children, ...props }, ref) => (
12 | <ScrollAreaPrimitive.Root
13 | ref={ref}
14 | className={cn('relative overflow-hidden', className)}
15 | {...props}
16 | >
17 | <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
18 | {children}
19 | </ScrollAreaPrimitive.Viewport>
20 | <ScrollBar />
21 | <ScrollAreaPrimitive.Corner />
22 | </ScrollAreaPrimitive.Root>
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
28 | React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
29 | >(({ className, orientation = 'vertical', ...props }, ref) => (
30 | <ScrollAreaPrimitive.ScrollAreaScrollbar
31 | ref={ref}
32 | orientation={orientation}
33 | className={cn(
34 | 'flex touch-none select-none transition-colors',
35 | orientation === 'vertical' &&
36 | 'h-full w-2.5 border-l border-l-transparent p-[1px]',
37 | orientation === 'horizontal' &&
38 | 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
39 | className
40 | )}
41 | {...props}
42 | >
43 | <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
44 | </ScrollAreaPrimitive.ScrollAreaScrollbar>
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
```
--------------------------------------------------------------------------------
/nextjs/app/api/cart/route.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { cosmosDBService } from '@/lib/cosmosdb';
3 |
4 | export async function GET(request: NextRequest) {
5 | try {
6 | // Get the userName from query parameters
7 | const searchParams = request.nextUrl.searchParams;
8 | const userName = searchParams.get('userName');
9 |
10 | if (!userName) {
11 | return NextResponse.json(
12 | { error: 'userName parameter is required' },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | // Load the cart from Cosmos DB
18 | const result = await cosmosDBService.loadCart(userName);
19 |
20 | if (result.error) {
21 | return NextResponse.json(
22 | { error: result.error },
23 | { status: result.statusCode }
24 | );
25 | }
26 |
27 | return NextResponse.json(result);
28 |
29 | } catch (error) {
30 | console.error('Error loading cart:', error);
31 | return NextResponse.json(
32 | { error: 'Failed to load cart' },
33 | { status: 500 }
34 | );
35 | }
36 | }
37 |
38 | export async function POST(request: NextRequest) {
39 | try {
40 | // Get the cart data from the request body
41 | const cart = await request.json();
42 |
43 | if (!cart || !cart.userName) {
44 | return NextResponse.json(
45 | { error: 'Invalid cart data. userName is required.' },
46 | { status: 400 }
47 | );
48 | }
49 |
50 | // Store the cart in Cosmos DB
51 | const result = await cosmosDBService.storeCart(cart);
52 |
53 | if (result.error) {
54 | return NextResponse.json(
55 | { error: result.error },
56 | { status: result.statusCode }
57 | );
58 | }
59 |
60 | return NextResponse.json(result);
61 |
62 | } catch (error) {
63 | console.error('Error storing cart:', error);
64 | return NextResponse.json(
65 | { error: 'Failed to store cart' },
66 | { status: 500 }
67 | );
68 | }
69 | }
```
--------------------------------------------------------------------------------
/nextjs/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import { GripVertical } from 'lucide-react';
4 | import * as ResizablePrimitive from 'react-resizable-panels';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
12 | <ResizablePrimitive.PanelGroup
13 | className={cn(
14 | 'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
15 | className
16 | )}
17 | {...props}
18 | />
19 | );
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel;
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
28 | withHandle?: boolean;
29 | }) => (
30 | <ResizablePrimitive.PanelResizeHandle
31 | className={cn(
32 | 'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 | <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
39 | <GripVertical className="h-2.5 w-2.5" />
40 | </div>
41 | )}
42 | </ResizablePrimitive.PanelResizeHandle>
43 | );
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
46 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
5 | import { type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 | import { toggleVariants } from '@/components/ui/toggle';
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps<typeof toggleVariants>
12 | >({
13 | size: 'default',
14 | variant: 'default',
15 | });
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef<typeof ToggleGroupPrimitive.Root>,
19 | React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
20 | VariantProps<typeof toggleVariants>
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 | <ToggleGroupPrimitive.Root
23 | ref={ref}
24 | className={cn('flex items-center justify-center gap-1', className)}
25 | {...props}
26 | >
27 | <ToggleGroupContext.Provider value={{ variant, size }}>
28 | {children}
29 | </ToggleGroupContext.Provider>
30 | </ToggleGroupPrimitive.Root>
31 | ));
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef<typeof ToggleGroupPrimitive.Item>,
37 | React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
38 | VariantProps<typeof toggleVariants>
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext);
41 |
42 | return (
43 | <ToggleGroupPrimitive.Item
44 | ref={ref}
45 | className={cn(
46 | toggleVariants({
47 | variant: context.variant || variant,
48 | size: context.size || size,
49 | }),
50 | className
51 | )}
52 | {...props}
53 | >
54 | {children}
55 | </ToggleGroupPrimitive.Item>
56 | );
57 | });
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
60 |
61 | export { ToggleGroup, ToggleGroupItem };
62 |
```
--------------------------------------------------------------------------------
/nextjs/app/layout.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import './globals.css';
2 |
3 | import { Bot, Mountain, User } from 'lucide-react';
4 |
5 | import AIAssistantDrawer from '@/components/AIAssistantDrawer';
6 | import CartItemCount from '@/components/CartItemCount';
7 | import { CartProvider } from '@/context/CartContext';
8 | import { Inter } from 'next/font/google';
9 | import Link from 'next/link';
10 | import type { Metadata } from 'next';
11 |
12 | const inter = Inter({ subsets: ['latin'] });
13 |
14 | export const metadata: Metadata = {
15 | title: 'Northern Mountains',
16 | description: 'Your adventure gear destination',
17 | };
18 |
19 | export default function RootLayout({
20 | children,
21 | }: {
22 | children: React.ReactNode;
23 | }) {
24 | return (
25 | <html lang="en">
26 | <body className={inter.className}>
27 | <CartProvider>
28 | <div className="flex flex-col min-h-screen">
29 | <header className="border-b">
30 | <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
31 | <div className="flex items-center justify-between">
32 | <Link href="/" className="flex items-center space-x-2">
33 | <Mountain className="h-8 w-8" />
34 | <span className="font-bold text-xl">Northern Mountains</span>
35 | <span className="font-bold text-xl text-red-500 pl-2">(East US 2)</span>
36 | </Link>
37 | <div className="flex items-center space-x-4">
38 | <AIAssistantDrawer />
39 | <Link href="/account" className="p-2 hover:bg-gray-100 rounded-full">
40 | <User className="h-6 w-6" />
41 | </Link>
42 | <CartItemCount />
43 | </div>
44 | </div>
45 | </div>
46 | </header>
47 | <main className="flex-grow relative">{children}</main>
48 | </div>
49 | </CartProvider>
50 | </body>
51 | </html>
52 | );
53 | }
```
--------------------------------------------------------------------------------
/mcp-server/src/cosmosdb.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CosmosClient } from '@azure/cosmos'
2 | import { DefaultAzureCredential } from '@azure/identity'
3 |
4 | const sourceFile = 'src/cosmosdb.ts'
5 |
6 | export function validateEnvironmentVariables () {
7 | const required = [
8 | 'AZURE_COSMOSDB_NOSQL_ENDPOINT',
9 | 'AZURE_COSMOSDB_NOSQL_DATABASE',
10 | 'AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER',
11 | 'AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER'
12 | ]
13 |
14 | for (const variable of required) {
15 | if (!process.env[variable]) {
16 | throw new Error(`Missing required environment variable: ${variable}`)
17 | }
18 | }
19 | }
20 |
21 | export const initializeCosmosDB = () => {
22 | const endpoint = process.env.AZURE_COSMOSDB_NOSQL_ENDPOINT as string
23 | const databaseId = process.env.AZURE_COSMOSDB_NOSQL_DATABASE as string
24 | const productsContainerId = process.env
25 | .AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER as string
26 | const cartsContainerId = process.env
27 | .AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER as string
28 | const ordersContainerId = process.env
29 | .AZURE_COSMOSDB_NOSQL_ORDERS_CONTAINER as string
30 |
31 | const credential = new DefaultAzureCredential()
32 |
33 | console.debug(
34 | `[${sourceFile}::initializeCosmosDB] Cosmos DB endpoint: ${endpoint}`
35 | )
36 | console.debug(
37 | `[${sourceFile}::initializeCosmosDB] Cosmos DB products container: ${productsContainerId}`
38 | )
39 | console.debug(
40 | `[${sourceFile}::initializeCosmosDB] Cosmos DB carts container: ${cartsContainerId}`
41 | )
42 | console.debug(
43 | `[${sourceFile}::initializeCosmosDB] Cosmos DB orders container: ${ordersContainerId}`
44 | )
45 |
46 | const client = new CosmosClient({
47 | endpoint: endpoint,
48 | aadCredentials: credential
49 | })
50 |
51 | const database = client.database(databaseId)
52 | const productsContainer = database.container(productsContainerId)
53 | const ordersContainer = database.container(ordersContainerId)
54 |
55 | return { client, database, productsContainer, ordersContainer }
56 | }
57 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/button.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38 | VariantProps<typeof buttonVariants> {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button';
45 | return (
46 | <Comp
47 | className={cn(buttonVariants({ variant, size, className }))}
48 | ref={ref}
49 | {...props}
50 | />
51 | );
52 | }
53 | );
54 | Button.displayName = 'Button';
55 |
56 | export { Button, buttonVariants };
57 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/card.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes<HTMLDivElement>
8 | >(({ className, ...props }, ref) => (
9 | <div
10 | ref={ref}
11 | className={cn(
12 | 'rounded-lg border bg-card text-card-foreground shadow-sm',
13 | className
14 | )}
15 | {...props}
16 | />
17 | ));
18 | Card.displayName = 'Card';
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes<HTMLDivElement>
23 | >(({ className, ...props }, ref) => (
24 | <div
25 | ref={ref}
26 | className={cn('flex flex-col space-y-1.5 p-6', className)}
27 | {...props}
28 | />
29 | ));
30 | CardHeader.displayName = 'CardHeader';
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes<HTMLHeadingElement>
35 | >(({ className, ...props }, ref) => (
36 | <h3
37 | ref={ref}
38 | className={cn(
39 | 'text-2xl font-semibold leading-none tracking-tight',
40 | className
41 | )}
42 | {...props}
43 | />
44 | ));
45 | CardTitle.displayName = 'CardTitle';
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes<HTMLParagraphElement>
50 | >(({ className, ...props }, ref) => (
51 | <p
52 | ref={ref}
53 | className={cn('text-sm text-muted-foreground', className)}
54 | {...props}
55 | />
56 | ));
57 | CardDescription.displayName = 'CardDescription';
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes<HTMLDivElement>
62 | >(({ className, ...props }, ref) => (
63 | <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
64 | ));
65 | CardContent.displayName = 'CardContent';
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes<HTMLDivElement>
70 | >(({ className, ...props }, ref) => (
71 | <div
72 | ref={ref}
73 | className={cn('flex items-center p-6 pt-0', className)}
74 | {...props}
75 | />
76 | ));
77 | CardFooter.displayName = 'CardFooter';
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TabsPrimitive from '@radix-ui/react-tabs';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef<typeof TabsPrimitive.List>,
12 | React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
13 | >(({ className, ...props }, ref) => (
14 | <TabsPrimitive.List
15 | ref={ref}
16 | className={cn(
17 | 'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
18 | className
19 | )}
20 | {...props}
21 | />
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef<typeof TabsPrimitive.Trigger>,
27 | React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
28 | >(({ className, ...props }, ref) => (
29 | <TabsPrimitive.Trigger
30 | ref={ref}
31 | className={cn(
32 | 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
33 | className
34 | )}
35 | {...props}
36 | />
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef<typeof TabsPrimitive.Content>,
42 | React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
43 | >(({ className, ...props }, ref) => (
44 | <TabsPrimitive.Content
45 | ref={ref}
46 | className={cn(
47 | 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
48 | className
49 | )}
50 | {...props}
51 | />
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
```
--------------------------------------------------------------------------------
/nextjs/app/globals.css:
--------------------------------------------------------------------------------
```css
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | @layer base {
20 | :root {
21 | --background: 0 0% 100%;
22 | --foreground: 0 0% 3.9%;
23 | --card: 0 0% 100%;
24 | --card-foreground: 0 0% 3.9%;
25 | --popover: 0 0% 100%;
26 | --popover-foreground: 0 0% 3.9%;
27 | --primary: 0 0% 9%;
28 | --primary-foreground: 0 0% 98%;
29 | --secondary: 0 0% 96.1%;
30 | --secondary-foreground: 0 0% 9%;
31 | --muted: 0 0% 96.1%;
32 | --muted-foreground: 0 0% 45.1%;
33 | --accent: 0 0% 96.1%;
34 | --accent-foreground: 0 0% 9%;
35 | --destructive: 0 84.2% 60.2%;
36 | --destructive-foreground: 0 0% 98%;
37 | --border: 0 0% 89.8%;
38 | --input: 0 0% 89.8%;
39 | --ring: 0 0% 3.9%;
40 | --chart-1: 12 76% 61%;
41 | --chart-2: 173 58% 39%;
42 | --chart-3: 197 37% 24%;
43 | --chart-4: 43 74% 66%;
44 | --chart-5: 27 87% 67%;
45 | --radius: 0.5rem;
46 | }
47 | .dark {
48 | --background: 0 0% 3.9%;
49 | --foreground: 0 0% 98%;
50 | --card: 0 0% 3.9%;
51 | --card-foreground: 0 0% 98%;
52 | --popover: 0 0% 3.9%;
53 | --popover-foreground: 0 0% 98%;
54 | --primary: 0 0% 98%;
55 | --primary-foreground: 0 0% 9%;
56 | --secondary: 0 0% 14.9%;
57 | --secondary-foreground: 0 0% 98%;
58 | --muted: 0 0% 14.9%;
59 | --muted-foreground: 0 0% 63.9%;
60 | --accent: 0 0% 14.9%;
61 | --accent-foreground: 0 0% 98%;
62 | --destructive: 0 62.8% 30.6%;
63 | --destructive-foreground: 0 0% 98%;
64 | --border: 0 0% 14.9%;
65 | --input: 0 0% 14.9%;
66 | --ring: 0 0% 83.1%;
67 | --chart-1: 220 70% 50%;
68 | --chart-2: 160 60% 45%;
69 | --chart-3: 30 80% 55%;
70 | --chart-4: 280 65% 60%;
71 | --chart-5: 340 75% 55%;
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
83 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion';
5 | import { ChevronDown } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef<typeof AccordionPrimitive.Item>,
13 | React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14 | >(({ className, ...props }, ref) => (
15 | <AccordionPrimitive.Item
16 | ref={ref}
17 | className={cn('border-b', className)}
18 | {...props}
19 | />
20 | ));
21 | AccordionItem.displayName = 'AccordionItem';
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef<typeof AccordionPrimitive.Trigger>,
25 | React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26 | >(({ className, children, ...props }, ref) => (
27 | <AccordionPrimitive.Header className="flex">
28 | <AccordionPrimitive.Trigger
29 | ref={ref}
30 | className={cn(
31 | 'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 | <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38 | </AccordionPrimitive.Trigger>
39 | </AccordionPrimitive.Header>
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef<typeof AccordionPrimitive.Content>,
45 | React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46 | >(({ className, children, ...props }, ref) => (
47 | <AccordionPrimitive.Content
48 | ref={ref}
49 | className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
50 | {...props}
51 | >
52 | <div className={cn('pb-4 pt-0', className)}>{children}</div>
53 | </AccordionPrimitive.Content>
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { OTPInput, OTPInputContext } from 'input-otp';
5 | import { Dot } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef<typeof OTPInput>,
11 | React.ComponentPropsWithoutRef<typeof OTPInput>
12 | >(({ className, containerClassName, ...props }, ref) => (
13 | <OTPInput
14 | ref={ref}
15 | containerClassName={cn(
16 | 'flex items-center gap-2 has-[:disabled]:opacity-50',
17 | containerClassName
18 | )}
19 | className={cn('disabled:cursor-not-allowed', className)}
20 | {...props}
21 | />
22 | ));
23 | InputOTP.displayName = 'InputOTP';
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<'div'>,
27 | React.ComponentPropsWithoutRef<'div'>
28 | >(({ className, ...props }, ref) => (
29 | <div ref={ref} className={cn('flex items-center', className)} {...props} />
30 | ));
31 | InputOTPGroup.displayName = 'InputOTPGroup';
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<'div'>,
35 | React.ComponentPropsWithoutRef<'div'> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext);
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
39 |
40 | return (
41 | <div
42 | ref={ref}
43 | className={cn(
44 | 'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
45 | isActive && 'z-10 ring-2 ring-ring ring-offset-background',
46 | className
47 | )}
48 | {...props}
49 | >
50 | {char}
51 | {hasFakeCaret && (
52 | <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
53 | <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
54 | </div>
55 | )}
56 | </div>
57 | );
58 | });
59 | InputOTPSlot.displayName = 'InputOTPSlot';
60 |
61 | const InputOTPSeparator = React.forwardRef<
62 | React.ElementRef<'div'>,
63 | React.ComponentPropsWithoutRef<'div'>
64 | >(({ ...props }, ref) => (
65 | <div ref={ref} role="separator" {...props}>
66 | <Dot />
67 | </div>
68 | ));
69 | InputOTPSeparator.displayName = 'InputOTPSeparator';
70 |
71 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
72 |
```
--------------------------------------------------------------------------------
/nextjs/tailwind.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | darkMode: ['class'],
5 | content: [
6 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
7 | './components/**/*.{js,ts,jsx,tsx,mdx}',
8 | './app/**/*.{js,ts,jsx,tsx,mdx}',
9 | ],
10 | theme: {
11 | extend: {
12 | backgroundImage: {
13 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
14 | 'gradient-conic':
15 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
16 | },
17 | borderRadius: {
18 | lg: 'var(--radius)',
19 | md: 'calc(var(--radius) - 2px)',
20 | sm: 'calc(var(--radius) - 4px)',
21 | },
22 | colors: {
23 | background: 'hsl(var(--background))',
24 | foreground: 'hsl(var(--foreground))',
25 | card: {
26 | DEFAULT: 'hsl(var(--card))',
27 | foreground: 'hsl(var(--card-foreground))',
28 | },
29 | popover: {
30 | DEFAULT: 'hsl(var(--popover))',
31 | foreground: 'hsl(var(--popover-foreground))',
32 | },
33 | primary: {
34 | DEFAULT: 'hsl(var(--primary))',
35 | foreground: 'hsl(var(--primary-foreground))',
36 | },
37 | secondary: {
38 | DEFAULT: 'hsl(var(--secondary))',
39 | foreground: 'hsl(var(--secondary-foreground))',
40 | },
41 | muted: {
42 | DEFAULT: 'hsl(var(--muted))',
43 | foreground: 'hsl(var(--muted-foreground))',
44 | },
45 | accent: {
46 | DEFAULT: 'hsl(var(--accent))',
47 | foreground: 'hsl(var(--accent-foreground))',
48 | },
49 | destructive: {
50 | DEFAULT: 'hsl(var(--destructive))',
51 | foreground: 'hsl(var(--destructive-foreground))',
52 | },
53 | border: 'hsl(var(--border))',
54 | input: 'hsl(var(--input))',
55 | ring: 'hsl(var(--ring))',
56 | chart: {
57 | '1': 'hsl(var(--chart-1))',
58 | '2': 'hsl(var(--chart-2))',
59 | '3': 'hsl(var(--chart-3))',
60 | '4': 'hsl(var(--chart-4))',
61 | '5': 'hsl(var(--chart-5))',
62 | },
63 | },
64 | keyframes: {
65 | 'accordion-down': {
66 | from: {
67 | height: '0',
68 | },
69 | to: {
70 | height: 'var(--radix-accordion-content-height)',
71 | },
72 | },
73 | 'accordion-up': {
74 | from: {
75 | height: 'var(--radix-accordion-content-height)',
76 | },
77 | to: {
78 | height: '0',
79 | },
80 | },
81 | },
82 | animation: {
83 | 'accordion-down': 'accordion-down 0.2s ease-out',
84 | 'accordion-up': 'accordion-up 0.2s ease-out',
85 | },
86 | },
87 | },
88 | plugins: [require('tailwindcss-animate')],
89 | };
90 | export default config;
91 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ChevronLeft, ChevronRight } from 'lucide-react';
5 | import { DayPicker } from 'react-day-picker';
6 |
7 | import { cn } from '@/lib/utils';
8 | import { buttonVariants } from '@/components/ui/button';
9 |
10 | export type CalendarProps = React.ComponentProps<typeof DayPicker>;
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | <DayPicker
20 | showOutsideDays={showOutsideDays}
21 | className={cn('p-3', className)}
22 | classNames={{
23 | months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
24 | month: 'space-y-4',
25 | caption: 'flex justify-center pt-1 relative items-center',
26 | caption_label: 'text-sm font-medium',
27 | nav: 'space-x-1 flex items-center',
28 | nav_button: cn(
29 | buttonVariants({ variant: 'outline' }),
30 | 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
31 | ),
32 | nav_button_previous: 'absolute left-1',
33 | nav_button_next: 'absolute right-1',
34 | table: 'w-full border-collapse space-y-1',
35 | head_row: 'flex',
36 | head_cell:
37 | 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
38 | row: 'flex w-full mt-2',
39 | cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
40 | day: cn(
41 | buttonVariants({ variant: 'ghost' }),
42 | 'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
43 | ),
44 | day_range_end: 'day-range-end',
45 | day_selected:
46 | 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
47 | day_today: 'bg-accent text-accent-foreground',
48 | day_outside:
49 | 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
50 | day_disabled: 'text-muted-foreground opacity-50',
51 | day_range_middle:
52 | 'aria-selected:bg-accent aria-selected:text-accent-foreground',
53 | day_hidden: 'invisible',
54 | ...classNames,
55 | }}
56 | components={{
57 | IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
58 | IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
59 | }}
60 | {...props}
61 | />
62 | );
63 | }
64 | Calendar.displayName = 'Calendar';
65 |
66 | export { Calendar };
67 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { ChevronRight, MoreHorizontal } from 'lucide-react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<'nav'> & {
10 | separator?: React.ReactNode;
11 | }
12 | >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13 | Breadcrumb.displayName = 'Breadcrumb';
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<'ol'>
18 | >(({ className, ...props }, ref) => (
19 | <ol
20 | ref={ref}
21 | className={cn(
22 | 'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
23 | className
24 | )}
25 | {...props}
26 | />
27 | ));
28 | BreadcrumbList.displayName = 'BreadcrumbList';
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<'li'>
33 | >(({ className, ...props }, ref) => (
34 | <li
35 | ref={ref}
36 | className={cn('inline-flex items-center gap-1.5', className)}
37 | {...props}
38 | />
39 | ));
40 | BreadcrumbItem.displayName = 'BreadcrumbItem';
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<'a'> & {
45 | asChild?: boolean;
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : 'a';
49 |
50 | return (
51 | <Comp
52 | ref={ref}
53 | className={cn('transition-colors hover:text-foreground', className)}
54 | {...props}
55 | />
56 | );
57 | });
58 | BreadcrumbLink.displayName = 'BreadcrumbLink';
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<'span'>
63 | >(({ className, ...props }, ref) => (
64 | <span
65 | ref={ref}
66 | role="link"
67 | aria-disabled="true"
68 | aria-current="page"
69 | className={cn('font-normal text-foreground', className)}
70 | {...props}
71 | />
72 | ));
73 | BreadcrumbPage.displayName = 'BreadcrumbPage';
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<'li'>) => (
80 | <li
81 | role="presentation"
82 | aria-hidden="true"
83 | className={cn('[&>svg]:size-3.5', className)}
84 | {...props}
85 | >
86 | {children ?? <ChevronRight />}
87 | </li>
88 | );
89 | BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<'span'>) => (
95 | <span
96 | role="presentation"
97 | aria-hidden="true"
98 | className={cn('flex h-9 w-9 items-center justify-center', className)}
99 | {...props}
100 | >
101 | <MoreHorizontal className="h-4 w-4" />
102 | <span className="sr-only">More</span>
103 | </span>
104 | );
105 | BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | };
116 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
3 |
4 | import { cn } from '@/lib/utils';
5 | import { ButtonProps, buttonVariants } from '@/components/ui/button';
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
8 | <nav
9 | role="navigation"
10 | aria-label="pagination"
11 | className={cn('mx-auto flex w-full justify-center', className)}
12 | {...props}
13 | />
14 | );
15 | Pagination.displayName = 'Pagination';
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<'ul'>
20 | >(({ className, ...props }, ref) => (
21 | <ul
22 | ref={ref}
23 | className={cn('flex flex-row items-center gap-1', className)}
24 | {...props}
25 | />
26 | ));
27 | PaginationContent.displayName = 'PaginationContent';
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<'li'>
32 | >(({ className, ...props }, ref) => (
33 | <li ref={ref} className={cn('', className)} {...props} />
34 | ));
35 | PaginationItem.displayName = 'PaginationItem';
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean;
39 | } & Pick<ButtonProps, 'size'> &
40 | React.ComponentProps<'a'>;
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = 'icon',
46 | ...props
47 | }: PaginationLinkProps) => (
48 | <a
49 | aria-current={isActive ? 'page' : undefined}
50 | className={cn(
51 | buttonVariants({
52 | variant: isActive ? 'outline' : 'ghost',
53 | size,
54 | }),
55 | className
56 | )}
57 | {...props}
58 | />
59 | );
60 | PaginationLink.displayName = 'PaginationLink';
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps<typeof PaginationLink>) => (
66 | <PaginationLink
67 | aria-label="Go to previous page"
68 | size="default"
69 | className={cn('gap-1 pl-2.5', className)}
70 | {...props}
71 | >
72 | <ChevronLeft className="h-4 w-4" />
73 | <span>Previous</span>
74 | </PaginationLink>
75 | );
76 | PaginationPrevious.displayName = 'PaginationPrevious';
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps<typeof PaginationLink>) => (
82 | <PaginationLink
83 | aria-label="Go to next page"
84 | size="default"
85 | className={cn('gap-1 pr-2.5', className)}
86 | {...props}
87 | >
88 | <span>Next</span>
89 | <ChevronRight className="h-4 w-4" />
90 | </PaginationLink>
91 | );
92 | PaginationNext.displayName = 'PaginationNext';
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<'span'>) => (
98 | <span
99 | aria-hidden
100 | className={cn('flex h-9 w-9 items-center justify-center', className)}
101 | {...props}
102 | >
103 | <MoreHorizontal className="h-4 w-4" />
104 | <span className="sr-only">More pages</span>
105 | </span>
106 | );
107 | PaginationEllipsis.displayName = 'PaginationEllipsis';
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | };
118 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/table.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes<HTMLTableElement>
8 | >(({ className, ...props }, ref) => (
9 | <div className="relative w-full overflow-auto">
10 | <table
11 | ref={ref}
12 | className={cn('w-full caption-bottom text-sm', className)}
13 | {...props}
14 | />
15 | </div>
16 | ));
17 | Table.displayName = 'Table';
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes<HTMLTableSectionElement>
22 | >(({ className, ...props }, ref) => (
23 | <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
24 | ));
25 | TableHeader.displayName = 'TableHeader';
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes<HTMLTableSectionElement>
30 | >(({ className, ...props }, ref) => (
31 | <tbody
32 | ref={ref}
33 | className={cn('[&_tr:last-child]:border-0', className)}
34 | {...props}
35 | />
36 | ));
37 | TableBody.displayName = 'TableBody';
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes<HTMLTableSectionElement>
42 | >(({ className, ...props }, ref) => (
43 | <tfoot
44 | ref={ref}
45 | className={cn(
46 | 'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
47 | className
48 | )}
49 | {...props}
50 | />
51 | ));
52 | TableFooter.displayName = 'TableFooter';
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes<HTMLTableRowElement>
57 | >(({ className, ...props }, ref) => (
58 | <tr
59 | ref={ref}
60 | className={cn(
61 | 'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
62 | className
63 | )}
64 | {...props}
65 | />
66 | ));
67 | TableRow.displayName = 'TableRow';
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes<HTMLTableCellElement>
72 | >(({ className, ...props }, ref) => (
73 | <th
74 | ref={ref}
75 | className={cn(
76 | 'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
77 | className
78 | )}
79 | {...props}
80 | />
81 | ));
82 | TableHead.displayName = 'TableHead';
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes<HTMLTableCellElement>
87 | >(({ className, ...props }, ref) => (
88 | <td
89 | ref={ref}
90 | className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
91 | {...props}
92 | />
93 | ));
94 | TableCell.displayName = 'TableCell';
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes<HTMLTableCaptionElement>
99 | >(({ className, ...props }, ref) => (
100 | <caption
101 | ref={ref}
102 | className={cn('mt-4 text-sm text-muted-foreground', className)}
103 | {...props}
104 | />
105 | ));
106 | TableCaption.displayName = 'TableCaption';
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | };
118 |
```
--------------------------------------------------------------------------------
/nextjs/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack -p 3002",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@azure/cosmos": "^4.2.0",
13 | "@azure/identity": "^4.6.0",
14 | "@azure/storage-blob": "^12.26.0",
15 | "@fastify/cors": "^10.0.2",
16 | "@hookform/resolvers": "^3.9.0",
17 | "@modelcontextprotocol/sdk": "^1.8.0",
18 | "@next/swc-wasm-nodejs": "13.5.1",
19 | "@radix-ui/react-accordion": "^1.2.0",
20 | "@radix-ui/react-alert-dialog": "^1.1.1",
21 | "@radix-ui/react-aspect-ratio": "^1.1.0",
22 | "@radix-ui/react-avatar": "^1.1.0",
23 | "@radix-ui/react-checkbox": "^1.1.1",
24 | "@radix-ui/react-collapsible": "^1.1.0",
25 | "@radix-ui/react-context-menu": "^2.2.1",
26 | "@radix-ui/react-dialog": "^1.1.1",
27 | "@radix-ui/react-dropdown-menu": "^2.1.1",
28 | "@radix-ui/react-hover-card": "^1.1.1",
29 | "@radix-ui/react-label": "^2.1.0",
30 | "@radix-ui/react-menubar": "^1.1.1",
31 | "@radix-ui/react-navigation-menu": "^1.2.0",
32 | "@radix-ui/react-popover": "^1.1.1",
33 | "@radix-ui/react-progress": "^1.1.0",
34 | "@radix-ui/react-radio-group": "^1.2.0",
35 | "@radix-ui/react-scroll-area": "^1.1.0",
36 | "@radix-ui/react-select": "^2.1.1",
37 | "@radix-ui/react-separator": "^1.1.0",
38 | "@radix-ui/react-slider": "^1.2.0",
39 | "@radix-ui/react-slot": "^1.1.0",
40 | "@radix-ui/react-switch": "^1.1.0",
41 | "@radix-ui/react-tabs": "^1.1.0",
42 | "@radix-ui/react-toast": "^1.2.1",
43 | "@radix-ui/react-toggle": "^1.1.0",
44 | "@radix-ui/react-toggle-group": "^1.1.0",
45 | "@radix-ui/react-tooltip": "^1.1.2",
46 | "@types/node": "20.6.2",
47 | "@types/react": "19.0.8",
48 | "@types/react-dom": "19.0.3",
49 | "autoprefixer": "10.4.15",
50 | "axios": "^1.7.9",
51 | "azure-storage": "^2.10.7",
52 | "class-variance-authority": "^0.7.0",
53 | "clsx": "^2.1.1",
54 | "cmdk": "^1.0.0",
55 | "date-fns": "^3.6.0",
56 | "dotenv": "^16.4.7",
57 | "embla-carousel-react": "^8.3.0",
58 | "eslint": "8.49.0",
59 | "eslint-config-next": "15.1.6",
60 | "express": "^5.0.1",
61 | "input-otp": "^1.2.4",
62 | "lucide-react": "^0.446.0",
63 | "next": "15.1.6",
64 | "next-themes": "^0.3.0",
65 | "openai": "^4.90.0",
66 | "postcss": "8.4.30",
67 | "re-resizable": "^6.11.2",
68 | "react": "19.0.0",
69 | "react-day-picker": "^8.10.1",
70 | "react-dom": "19.0.0",
71 | "react-hook-form": "^7.53.0",
72 | "react-markdown": "^10.1.0",
73 | "react-resizable-panels": "^2.1.3",
74 | "recharts": "^2.12.7",
75 | "sonner": "^1.5.0",
76 | "tailwind-merge": "^2.5.2",
77 | "tailwindcss": "3.3.3",
78 | "tailwindcss-animate": "^1.0.7",
79 | "typescript": "5.2.2",
80 | "uuid": "^11.0.5",
81 | "vaul": "^0.9.9",
82 | "zod": "^3.24.2"
83 | },
84 | "devDependencies": {},
85 | "overrides": {
86 | "@types/react": "19.0.8",
87 | "@types/react-dom": "19.0.3"
88 | }
89 | }
90 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { Drawer as DrawerPrimitive } from 'vaul';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
12 | <DrawerPrimitive.Root
13 | shouldScaleBackground={shouldScaleBackground}
14 | {...props}
15 | />
16 | );
17 | Drawer.displayName = 'Drawer';
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger;
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal;
22 |
23 | const DrawerClose = DrawerPrimitive.Close;
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef<typeof DrawerPrimitive.Overlay>,
27 | React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
28 | >(({ className, ...props }, ref) => (
29 | <DrawerPrimitive.Overlay
30 | ref={ref}
31 | className={cn('fixed inset-0 z-50 bg-black/80', className)}
32 | {...props}
33 | />
34 | ));
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef<typeof DrawerPrimitive.Content>,
39 | React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
40 | >(({ className, children, ...props }, ref) => (
41 | <DrawerPortal>
42 | <DrawerOverlay />
43 | <DrawerPrimitive.Content
44 | ref={ref}
45 | className={cn(
46 | 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
47 | className
48 | )}
49 | {...props}
50 | >
51 | <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
52 | {children}
53 | </DrawerPrimitive.Content>
54 | </DrawerPortal>
55 | ));
56 | DrawerContent.displayName = 'DrawerContent';
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes<HTMLDivElement>) => (
62 | <div
63 | className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}
64 | {...props}
65 | />
66 | );
67 | DrawerHeader.displayName = 'DrawerHeader';
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes<HTMLDivElement>) => (
73 | <div
74 | className={cn('mt-auto flex flex-col gap-2 p-4', className)}
75 | {...props}
76 | />
77 | );
78 | DrawerFooter.displayName = 'DrawerFooter';
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef<typeof DrawerPrimitive.Title>,
82 | React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
83 | >(({ className, ...props }, ref) => (
84 | <DrawerPrimitive.Title
85 | ref={ref}
86 | className={cn(
87 | 'text-lg font-semibold leading-none tracking-tight',
88 | className
89 | )}
90 | {...props}
91 | />
92 | ));
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef<typeof DrawerPrimitive.Description>,
97 | React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
98 | >(({ className, ...props }, ref) => (
99 | <DrawerPrimitive.Description
100 | ref={ref}
101 | className={cn('text-sm text-muted-foreground', className)}
102 | {...props}
103 | />
104 | ));
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | };
119 |
```
--------------------------------------------------------------------------------
/nextjs/app/orders/[id]/page.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react'
4 | import Image from 'next/image'
5 | import { Button } from '@/components/ui/button'
6 | import { useRouter } from 'next/navigation'
7 |
8 | export default function OrderDetailsPage({ params }: { params: { id: string } }) {
9 | const [order, setOrder] = useState<any>(null)
10 | const [loading, setLoading] = useState(true)
11 | const [imageUrls, setImageUrls] = useState<{ [key: string]: string }>({})
12 | const router = useRouter()
13 |
14 | useEffect(() => {
15 | async function fetchOrder() {
16 | try {
17 | const response = await fetch(`/api/orders/${params.id}`)
18 | if (response.ok) {
19 | const data = await response.json()
20 | setOrder(data.data)
21 | // Fetch images for all items
22 | data.data.items.forEach(async (item: any) => {
23 | const imgResponse = await fetch(`/api/blob-url?blob=${item.id}.webp`, {
24 | cache: 'force-cache'
25 | })
26 | const imgData = await imgResponse.json()
27 | if (imgData.url) {
28 | setImageUrls(prev => ({
29 | ...prev,
30 | [item.id]: imgData.url
31 | }))
32 | }
33 | })
34 | }
35 | } catch (error) {
36 | console.error('Error fetching order:', error)
37 | } finally {
38 | setLoading(false)
39 | }
40 | }
41 |
42 | fetchOrder()
43 | }, [params.id])
44 |
45 | if (loading) {
46 | return <div>Loading order details...</div>
47 | }
48 |
49 | if (!order) {
50 | return <div>Order not found</div>
51 | }
52 |
53 | return (
54 | <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
55 | <div className="mb-8">
56 | <h1 className="text-2xl font-bold mb-2">Order Confirmation</h1>
57 | <p className="text-gray-600">Order #{order.id}</p>
58 | <p className="text-gray-600">Status: {order.status}</p>
59 | <p className="text-gray-600">Date: {new Date(order.createdAt).toLocaleDateString()}</p>
60 | </div>
61 |
62 | <div className="bg-white shadow overflow-hidden sm:rounded-lg">
63 | <div className="px-4 py-5 sm:px-6">
64 | <h2 className="text-lg font-medium">Order Items</h2>
65 | </div>
66 | <div className="border-t border-gray-200">
67 | {order.items.map((item: any) => (
68 | <div key={item.id} className="flex items-center p-4 border-b">
69 | <Image
70 | src={imageUrls[item.id] || '/placeholder-300x300.png'}
71 | alt={item.name}
72 | className="w-20 h-20 object-cover rounded"
73 | width={80}
74 | height={80}
75 | unoptimized
76 | />
77 | <div className="ml-4 flex-1">
78 | <h3 className="font-medium">{item.name}</h3>
79 | <p className="text-gray-500">Quantity: {item.quantity}</p>
80 | <p className="text-gray-500">${item.price} each</p>
81 | </div>
82 | <div className="text-right">
83 | <p className="font-medium">${(item.price * item.quantity).toFixed(2)}</p>
84 | </div>
85 | </div>
86 | ))}
87 | </div>
88 | <div className="px-4 py-5 sm:px-6">
89 | <div className="flex justify-between">
90 | <span className="font-medium">Total</span>
91 | <span className="font-medium">${order.total.toFixed(2)}</span>
92 | </div>
93 | </div>
94 | </div>
95 |
96 | <Button
97 | className="mt-8"
98 | onClick={() => router.push('/')}
99 | >
100 | Continue Shopping
101 | </Button>
102 | </div>
103 | )
104 | }
```
--------------------------------------------------------------------------------
/nextjs/context/CartContext.tsx:
--------------------------------------------------------------------------------
```typescript
1 | // app/context/CartContext.tsx
2 |
3 | 'use client'
4 |
5 | export const dynamic = 'force-dynamic' // Ensure dynamic rendering
6 |
7 | import { createContext, useContext, useEffect, useState } from 'react'
8 |
9 | import { Cart } from '@/models/cart'
10 | import { CartContextType } from '@/models/cartContextType'
11 | import { CartItem } from '@/models/cartItem'
12 | import { EMAIL } from '@/models/constants'
13 |
14 | export const CartContext = createContext<CartContextType | undefined>(undefined)
15 | const prefix = '/context/CartContext.tsx'
16 |
17 | export function CartProvider ({ children }: { children: React.ReactNode }) {
18 | const [items, setItems] = useState<CartItem[]>([])
19 |
20 | useEffect(() => {
21 | async function loadCart () {
22 | try {
23 | const savedCart = await loadCartFromCosmosDB(EMAIL)
24 | if (savedCart?.data?.items) {
25 | console.log(`[${prefix}::loadCart]`, savedCart.data.items)
26 | setItems(savedCart.data.items)
27 | }
28 | } catch (error) {
29 | console.error('Error loading cart:', error)
30 | }
31 | }
32 |
33 | loadCart()
34 | }, []) // Remove isSocketUpdate dependency
35 |
36 | useEffect(() => {
37 | console.log('Items state changed:', items)
38 | let isStale = false
39 |
40 | const storeCart = async () => {
41 | if (isStale) return
42 | try {
43 | const cart: Cart = {
44 | userName: EMAIL,
45 | items: items
46 | }
47 | await storeCartInCosmosDB(cart)
48 | } catch (error) {
49 | console.error('Error managing cart:', error)
50 | }
51 | }
52 |
53 | storeCart()
54 |
55 | return () => {
56 | isStale = true
57 | }
58 | }, [items])
59 |
60 | const addItem = (newItem: Omit<CartItem, 'quantity'>) => {
61 | setItems(currentItems => {
62 | const existingItem = currentItems.find(item => item.id === newItem.id)
63 | if (existingItem) {
64 | return currentItems.map(item =>
65 | item.id === newItem.id
66 | ? { ...item, quantity: item.quantity + 1 }
67 | : item
68 | )
69 | }
70 | return [...currentItems, { ...newItem, quantity: 1 }]
71 | })
72 | }
73 | const removeItem = (id: number) => {
74 | setItems(currentItems => currentItems.filter(item => item.id !== id))
75 | }
76 | const updateQuantity = (id: number, quantity: number) => {
77 | setItems(currentItems =>
78 | currentItems.map(item => (item.id === id ? { ...item, quantity } : item))
79 | )
80 | }
81 | const clearCart = () => {
82 | setItems([])
83 | }
84 | // load cart from Cosmos DB
85 | const loadCartFromCosmosDB = async (userName: string) => {
86 | try {
87 | const response = await fetch(`/api/cart?userName=${userName}`)
88 | const result = await response.json()
89 | console.log(`[${prefix}::loadCartFromCosmosDB] ${JSON.stringify(result)}`)
90 | return result
91 | } catch (error) {
92 | console.error('Error fetching cart:', error)
93 | return error
94 | }
95 | }
96 | // Function to store cart in Cosmos DB
97 | const storeCartInCosmosDB = async (cart: Cart) => {
98 | try {
99 | console.log(`[${prefix}::storeCartInCosmosDB] ${JSON.stringify(cart)}`)
100 | const response = await fetch(`/api/cart`, {
101 | method: 'POST',
102 | headers: {
103 | 'Content-Type': 'application/json',
104 | 'X-Client-Update': 'true'
105 | },
106 | body: JSON.stringify(cart)
107 | })
108 | return await response.json()
109 | } catch (error) {
110 | console.error('Error storing cart in Cosmos DB:', error)
111 | throw error
112 | }
113 | }
114 |
115 | return (
116 | <CartContext.Provider
117 | value={{ items, addItem, removeItem, updateQuantity, clearCart }}
118 | >
119 | {children}
120 | </CartContext.Provider>
121 | )
122 | }
123 |
124 | export function useCart () {
125 | const context = useContext(CartContext)
126 | if (context === undefined) {
127 | throw new Error('useCart must be used within a CartProvider')
128 | }
129 | return context
130 | }
131 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { X } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef<typeof DialogPrimitive.Overlay>,
19 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20 | >(({ className, ...props }, ref) => (
21 | <DialogPrimitive.Overlay
22 | ref={ref}
23 | className={cn(
24 | 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
25 | className
26 | )}
27 | {...props}
28 | />
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef<typeof DialogPrimitive.Content>,
34 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35 | >(({ className, children, ...props }, ref) => (
36 | <DialogPortal>
37 | <DialogOverlay />
38 | <DialogPrimitive.Content
39 | ref={ref}
40 | className={cn(
41 | 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
42 | className
43 | )}
44 | {...props}
45 | >
46 | {children}
47 | <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48 | <X className="h-4 w-4" />
49 | <span className="sr-only">Close</span>
50 | </DialogPrimitive.Close>
51 | </DialogPrimitive.Content>
52 | </DialogPortal>
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes<HTMLDivElement>) => (
60 | <div
61 | className={cn(
62 | 'flex flex-col space-y-1.5 text-center sm:text-left',
63 | className
64 | )}
65 | {...props}
66 | />
67 | );
68 | DialogHeader.displayName = 'DialogHeader';
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes<HTMLDivElement>) => (
74 | <div
75 | className={cn(
76 | 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
77 | className
78 | )}
79 | {...props}
80 | />
81 | );
82 | DialogFooter.displayName = 'DialogFooter';
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef<typeof DialogPrimitive.Title>,
86 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87 | >(({ className, ...props }, ref) => (
88 | <DialogPrimitive.Title
89 | ref={ref}
90 | className={cn(
91 | 'text-lg font-semibold leading-none tracking-tight',
92 | className
93 | )}
94 | {...props}
95 | />
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef<typeof DialogPrimitive.Description>,
101 | React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102 | >(({ className, ...props }, ref) => (
103 | <DialogPrimitive.Description
104 | ref={ref}
105 | className={cn('text-sm text-muted-foreground', className)}
106 | {...props}
107 | />
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
```
--------------------------------------------------------------------------------
/nextjs/components/Products.tsx:
--------------------------------------------------------------------------------
```typescript
1 | // components/Products.tsx
2 |
3 | 'use client'
4 |
5 | import { useEffect, useState } from 'react'
6 |
7 | import { Button } from '@/components/ui/button'
8 | import Image from 'next/image'
9 | import { Product } from '@/models/product'
10 | import { useCart } from '@/context/CartContext'
11 | import { useRouter } from 'next/navigation'
12 | import { v4 as uuidv4 } from 'uuid' // Import the uuid library
13 |
14 | const prefix = 'components/Products.tsx'
15 |
16 | interface ProductsProps {
17 | productIds?: string[];
18 | }
19 |
20 | export default function Products({ productIds }: ProductsProps) {
21 | const { addItem } = useCart()
22 | const router = useRouter()
23 | const [products, setProducts] = useState<Product[]>([])
24 | const [loading, setLoading] = useState(true)
25 | const [imageUrls, setImageUrls] = useState<{ [key: string]: string }>({})
26 |
27 | useEffect(() => {
28 | async function fetchImageUrl (id: string) {
29 | try {
30 | const response = await fetch(`/api/blob-url?blob=${id}.webp`, {
31 | cache: 'force-cache'
32 | })
33 | const data = await response.json()
34 | if (data.url) {
35 | setImageUrls(prev => ({
36 | ...prev,
37 | [id]: data.url // This will now be a base64 string
38 | }))
39 | }
40 | } catch (error) {
41 | console.error('Error fetching image URL:', error)
42 | }
43 | }
44 |
45 | async function fetchProducts () {
46 | try {
47 | const token = uuidv4()
48 | let url = `/api/products?token=${token}`
49 |
50 | // If productIds are provided, add them as a query parameter
51 | if (productIds && productIds.length > 0) {
52 | url += `&ids=${productIds.join(',')}`
53 | }
54 |
55 | const response = await fetch(url)
56 | const result = await response.json()
57 | console.log(`[${prefix}] fetchProducts: ${result.duration} ms `)
58 | setProducts(result.data)
59 |
60 | // Fetch image URLs for all products
61 | for (const product of result.data) {
62 | fetchImageUrl(product.id)
63 | }
64 | } catch (error) {
65 | console.error('Error fetching products:', error)
66 | } finally {
67 | setLoading(false)
68 | }
69 | }
70 |
71 | fetchProducts()
72 | }, [productIds]) // Add productIds as a dependency
73 |
74 | const handleAddToCart = (product: Product) => {
75 | const cartItem = {
76 | ...product,
77 | id: Number(product.id) // Convert id to number
78 | }
79 | addItem(cartItem)
80 | // router.push('/cart')
81 | }
82 |
83 | if (loading) {
84 | return (
85 | <div className='grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-8'>
86 | {[...Array(6)].map((_, i) => (
87 | <div key={i} className='animate-pulse'>
88 | <div className='aspect-w-1 aspect-h-1 w-full bg-gray-200 rounded-lg mb-4' />
89 | <div className='h-4 bg-gray-200 rounded w-3/4 mb-2' />
90 | <div className='h-4 bg-gray-200 rounded w-1/4' />
91 | </div>
92 | ))}
93 | </div>
94 | )
95 | }
96 |
97 | return (
98 | <div className='grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-8'>
99 | {products.map(product => (
100 | <div key={product.id} className='group relative'>
101 | <div className='aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-200'>
102 | <Image
103 | src={imageUrls[product.id] || '/placeholder-300x300.png'}
104 | alt={product.name}
105 | className='h-full w-full object-cover object-center group-hover:opacity-75'
106 | width={300}
107 | height={300}
108 | unoptimized
109 | />
110 | </div>
111 | <div className='mt-4 flex justify-between'>
112 | <div>
113 | <h3 className='text-sm text-gray-700'>{product.name}</h3>
114 | <p className='mt-1 text-sm text-gray-500'>${product.price}</p>
115 | </div>
116 | </div>
117 | <Button
118 | onClick={() => handleAddToCart(product)}
119 | className='mt-4 w-full'
120 | >
121 | Add to Cart
122 | </Button>
123 | </div>
124 | ))}
125 | </div>
126 | )
127 | }
128 |
```
--------------------------------------------------------------------------------
/nextjs/hooks/use-toast.ts:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from 'react';
5 |
6 | import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
7 |
8 | const TOAST_LIMIT = 1;
9 | const TOAST_REMOVE_DELAY = 1000000;
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string;
13 | title?: React.ReactNode;
14 | description?: React.ReactNode;
15 | action?: ToastActionElement;
16 | };
17 |
18 | const actionTypes = {
19 | ADD_TOAST: 'ADD_TOAST',
20 | UPDATE_TOAST: 'UPDATE_TOAST',
21 | DISMISS_TOAST: 'DISMISS_TOAST',
22 | REMOVE_TOAST: 'REMOVE_TOAST',
23 | } as const;
24 |
25 | let count = 0;
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
29 | return count.toString();
30 | }
31 |
32 | type ActionType = typeof actionTypes;
33 |
34 | type Action =
35 | | {
36 | type: ActionType['ADD_TOAST'];
37 | toast: ToasterToast;
38 | }
39 | | {
40 | type: ActionType['UPDATE_TOAST'];
41 | toast: Partial<ToasterToast>;
42 | }
43 | | {
44 | type: ActionType['DISMISS_TOAST'];
45 | toastId?: ToasterToast['id'];
46 | }
47 | | {
48 | type: ActionType['REMOVE_TOAST'];
49 | toastId?: ToasterToast['id'];
50 | };
51 |
52 | interface State {
53 | toasts: ToasterToast[];
54 | }
55 |
56 | const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
57 |
58 | const addToRemoveQueue = (toastId: string) => {
59 | if (toastTimeouts.has(toastId)) {
60 | return;
61 | }
62 |
63 | const timeout = setTimeout(() => {
64 | toastTimeouts.delete(toastId);
65 | dispatch({
66 | type: 'REMOVE_TOAST',
67 | toastId: toastId,
68 | });
69 | }, TOAST_REMOVE_DELAY);
70 |
71 | toastTimeouts.set(toastId, timeout);
72 | };
73 |
74 | export const reducer = (state: State, action: Action): State => {
75 | switch (action.type) {
76 | case 'ADD_TOAST':
77 | return {
78 | ...state,
79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 | };
81 |
82 | case 'UPDATE_TOAST':
83 | return {
84 | ...state,
85 | toasts: state.toasts.map((t) =>
86 | t.id === action.toast.id ? { ...t, ...action.toast } : t
87 | ),
88 | };
89 |
90 | case 'DISMISS_TOAST': {
91 | const { toastId } = action;
92 |
93 | // ! Side effects ! - This could be extracted into a dismissToast() action,
94 | // but I'll keep it here for simplicity
95 | if (toastId) {
96 | addToRemoveQueue(toastId);
97 | } else {
98 | state.toasts.forEach((toast) => {
99 | addToRemoveQueue(toast.id);
100 | });
101 | }
102 |
103 | return {
104 | ...state,
105 | toasts: state.toasts.map((t) =>
106 | t.id === toastId || toastId === undefined
107 | ? {
108 | ...t,
109 | open: false,
110 | }
111 | : t
112 | ),
113 | };
114 | }
115 | case 'REMOVE_TOAST':
116 | if (action.toastId === undefined) {
117 | return {
118 | ...state,
119 | toasts: [],
120 | };
121 | }
122 | return {
123 | ...state,
124 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
125 | };
126 | }
127 | };
128 |
129 | const listeners: Array<(state: State) => void> = [];
130 |
131 | let memoryState: State = { toasts: [] };
132 |
133 | function dispatch(action: Action) {
134 | memoryState = reducer(memoryState, action);
135 | listeners.forEach((listener) => {
136 | listener(memoryState);
137 | });
138 | }
139 |
140 | type Toast = Omit<ToasterToast, 'id'>;
141 |
142 | function toast({ ...props }: Toast) {
143 | const id = genId();
144 |
145 | const update = (props: ToasterToast) =>
146 | dispatch({
147 | type: 'UPDATE_TOAST',
148 | toast: { ...props, id },
149 | });
150 | const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
151 |
152 | dispatch({
153 | type: 'ADD_TOAST',
154 | toast: {
155 | ...props,
156 | id,
157 | open: true,
158 | onOpenChange: (open) => {
159 | if (!open) dismiss();
160 | },
161 | },
162 | });
163 |
164 | return {
165 | id: id,
166 | dismiss,
167 | update,
168 | };
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState<State>(memoryState);
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState);
176 | return () => {
177 | const index = listeners.indexOf(setState);
178 | if (index > -1) {
179 | listeners.splice(index, 1);
180 | }
181 | };
182 | }, [state]);
183 |
184 | return {
185 | ...state,
186 | toast,
187 | dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
188 | };
189 | }
190 |
191 | export { useToast, toast };
192 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/form.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { Slot } from '@radix-ui/react-slot';
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from 'react-hook-form';
14 |
15 | import { cn } from '@/lib/utils';
16 | import { Label } from '@/components/ui/label';
17 |
18 | const Form = FormProvider;
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
23 | > = {
24 | name: TName;
25 | };
26 |
27 | const FormFieldContext = React.createContext<FormFieldContextValue>(
28 | {} as FormFieldContextValue
29 | );
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34 | >({
35 | ...props
36 | }: ControllerProps<TFieldValues, TName>) => {
37 | return (
38 | <FormFieldContext.Provider value={{ name: props.name }}>
39 | <Controller {...props} />
40 | </FormFieldContext.Provider>
41 | );
42 | };
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext);
46 | const itemContext = React.useContext(FormItemContext);
47 | const { getFieldState, formState } = useFormContext();
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState);
50 |
51 | if (!fieldContext) {
52 | throw new Error('useFormField should be used within <FormField>');
53 | }
54 |
55 | const { id } = itemContext;
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | };
65 | };
66 |
67 | type FormItemContextValue = {
68 | id: string;
69 | };
70 |
71 | const FormItemContext = React.createContext<FormItemContextValue>(
72 | {} as FormItemContextValue
73 | );
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes<HTMLDivElement>
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId();
80 |
81 | return (
82 | <FormItemContext.Provider value={{ id }}>
83 | <div ref={ref} className={cn('space-y-2', className)} {...props} />
84 | </FormItemContext.Provider>
85 | );
86 | });
87 | FormItem.displayName = 'FormItem';
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef<typeof LabelPrimitive.Root>,
91 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField();
94 |
95 | return (
96 | <Label
97 | ref={ref}
98 | className={cn(error && 'text-destructive', className)}
99 | htmlFor={formItemId}
100 | {...props}
101 | />
102 | );
103 | });
104 | FormLabel.displayName = 'FormLabel';
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef<typeof Slot>,
108 | React.ComponentPropsWithoutRef<typeof Slot>
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } =
111 | useFormField();
112 |
113 | return (
114 | <Slot
115 | ref={ref}
116 | id={formItemId}
117 | aria-describedby={
118 | !error
119 | ? `${formDescriptionId}`
120 | : `${formDescriptionId} ${formMessageId}`
121 | }
122 | aria-invalid={!!error}
123 | {...props}
124 | />
125 | );
126 | });
127 | FormControl.displayName = 'FormControl';
128 |
129 | const FormDescription = React.forwardRef<
130 | HTMLParagraphElement,
131 | React.HTMLAttributes<HTMLParagraphElement>
132 | >(({ className, ...props }, ref) => {
133 | const { formDescriptionId } = useFormField();
134 |
135 | return (
136 | <p
137 | ref={ref}
138 | id={formDescriptionId}
139 | className={cn('text-sm text-muted-foreground', className)}
140 | {...props}
141 | />
142 | );
143 | });
144 | FormDescription.displayName = 'FormDescription';
145 |
146 | const FormMessage = React.forwardRef<
147 | HTMLParagraphElement,
148 | React.HTMLAttributes<HTMLParagraphElement>
149 | >(({ className, children, ...props }, ref) => {
150 | const { error, formMessageId } = useFormField();
151 | const body = error ? String(error?.message) : children;
152 |
153 | if (!body) {
154 | return null;
155 | }
156 |
157 | return (
158 | <p
159 | ref={ref}
160 | id={formMessageId}
161 | className={cn('text-sm font-medium text-destructive', className)}
162 | {...props}
163 | >
164 | {body}
165 | </p>
166 | );
167 | });
168 | FormMessage.displayName = 'FormMessage';
169 |
170 | export {
171 | useFormField,
172 | Form,
173 | FormItem,
174 | FormLabel,
175 | FormControl,
176 | FormDescription,
177 | FormMessage,
178 | FormField,
179 | };
180 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
```typescript
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef<typeof SheetPrimitive.Overlay>,
20 | React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21 | >(({ className, ...props }, ref) => (
22 | <SheetPrimitive.Overlay
23 | className={cn(
24 | "fixed inset-0 z-50 pointer-events-none",
25 | className
26 | )}
27 | {...props}
28 | ref={ref}
29 | />
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
35 | {
36 | variants: {
37 | side: {
38 | top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
39 | bottom:
40 | 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
41 | left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
42 | right:
43 | 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
44 | },
45 | },
46 | defaultVariants: {
47 | side: 'right',
48 | },
49 | }
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54 | VariantProps<typeof sheetVariants> {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef<typeof SheetPrimitive.Content>,
58 | SheetContentProps
59 | >(({ side = 'right', className, children, ...props }, ref) => (
60 | <SheetPortal>
61 | <SheetOverlay />
62 | <SheetPrimitive.Content
63 | ref={ref}
64 | className={cn(sheetVariants({ side }), className)}
65 | onPointerDownOutside={(e) => {
66 | e.preventDefault();
67 | }}
68 | {...props}
69 | >
70 | {children}
71 | <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
72 | <X className="h-4 w-4" />
73 | <span className="sr-only">Close</span>
74 | </SheetPrimitive.Close>
75 | </SheetPrimitive.Content>
76 | </SheetPortal>
77 | ));
78 | SheetContent.displayName = SheetPrimitive.Content.displayName;
79 |
80 | const SheetHeader = ({
81 | className,
82 | ...props
83 | }: React.HTMLAttributes<HTMLDivElement>) => (
84 | <div
85 | className={cn(
86 | 'flex flex-col space-y-2 text-center sm:text-left',
87 | className
88 | )}
89 | {...props}
90 | />
91 | );
92 | SheetHeader.displayName = 'SheetHeader';
93 |
94 | const SheetFooter = ({
95 | className,
96 | ...props
97 | }: React.HTMLAttributes<HTMLDivElement>) => (
98 | <div
99 | className={cn(
100 | 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
101 | className
102 | )}
103 | {...props}
104 | />
105 | );
106 | SheetFooter.displayName = 'SheetFooter';
107 |
108 | const SheetTitle = React.forwardRef<
109 | React.ElementRef<typeof SheetPrimitive.Title>,
110 | React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
111 | >(({ className, ...props }, ref) => (
112 | <SheetPrimitive.Title
113 | ref={ref}
114 | className={cn('text-lg font-semibold text-foreground', className)}
115 | {...props}
116 | />
117 | ));
118 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
119 |
120 | const SheetDescription = React.forwardRef<
121 | React.ElementRef<typeof SheetPrimitive.Description>,
122 | React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
123 | >(({ className, ...props }, ref) => (
124 | <SheetPrimitive.Description
125 | ref={ref}
126 | className={cn('text-sm text-muted-foreground', className)}
127 | {...props}
128 | />
129 | ));
130 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
131 |
132 | export {
133 | Sheet,
134 | SheetPortal,
135 | SheetOverlay,
136 | SheetTrigger,
137 | SheetClose,
138 | SheetContent,
139 | SheetHeader,
140 | SheetFooter,
141 | SheetTitle,
142 | SheetDescription,
143 | };
144 |
```
--------------------------------------------------------------------------------
/nextjs/lib/cosmosdb.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Container, CosmosClient, SqlQuerySpec } from "@azure/cosmos";
2 |
3 | import { Cart } from "@/models/cart";
4 | import { DefaultAzureCredential } from "@azure/identity";
5 |
6 | const sourceFile: string = "lib/cosmosdb.ts";
7 |
8 | export class CosmosDBService {
9 | private client: CosmosClient;
10 | public database: any;
11 | public cartsContainer: Container;
12 | public productsContainer: Container;
13 | public ordersContainer: Container; // Add this line
14 |
15 | constructor() {
16 | const endpoint = process.env.AZURE_COSMOSDB_NOSQL_ENDPOINT as string;
17 | const databaseId = process.env.AZURE_COSMOSDB_NOSQL_DATABASE as string;
18 | const productsContainerId = process.env.AZURE_COSMOSDB_NOSQL_PRODUCTS_CONTAINER as string;
19 | const cartsContainerId = process.env.AZURE_COSMOSDB_NOSQL_CARTS_CONTAINER as string;
20 | const ordersContainerId = process.env.AZURE_COSMOSDB_NOSQL_ORDERS_CONTAINER as string;
21 |
22 | const credential = new DefaultAzureCredential();
23 |
24 | this.client = new CosmosClient({
25 | endpoint: endpoint,
26 | aadCredentials: credential,
27 | });
28 |
29 | this.database = this.client.database(databaseId);
30 | this.cartsContainer = this.database.container(cartsContainerId);
31 | this.productsContainer = this.database.container(productsContainerId);
32 | this.ordersContainer = this.database.container(ordersContainerId);
33 | }
34 |
35 | async storeCart(cart: Cart) {
36 | try {
37 | const sqlQuerySpec: SqlQuerySpec = {
38 | query: "SELECT TOP 1 * FROM c WHERE c.userName = @userName",
39 | parameters: [
40 | {
41 | name: "@userName",
42 | value: cart.userName,
43 | },
44 | ],
45 | };
46 | const { resources: items } = await this.cartsContainer.items.query(sqlQuerySpec).fetchAll();
47 |
48 | if (items.length > 0) {
49 | const existingCart = items[0];
50 | existingCart.items = cart.items;
51 | this.cartsContainer.items.upsert(existingCart);
52 | } else {
53 | this.cartsContainer.items.upsert(cart);
54 | }
55 |
56 | return {
57 | data: items,
58 | statusCode: 200,
59 | };
60 | } catch (error) {
61 | return {
62 | error: "Error storing cart in Cosmos DB: " + error,
63 | statusCode: 500,
64 | };
65 | }
66 | }
67 |
68 | async loadCart(userName: string) {
69 | try {
70 | const sqlQuerySpec: SqlQuerySpec = {
71 | query: "SELECT TOP 1 * FROM c WHERE c.userName = @userName",
72 | parameters: [
73 | {
74 | name: "@userName",
75 | value: userName,
76 | },
77 | ],
78 | };
79 | const { resources: items } = await this.cartsContainer.items.query(sqlQuerySpec).fetchAll();
80 |
81 | return {
82 | data: items,
83 | statusCode: 200,
84 | };
85 | } catch (error) {
86 | return {
87 | error: "Error loading cart from Cosmos DB: " + error,
88 | statusCode: 500,
89 | };
90 | }
91 | }
92 |
93 | async getProducts() {
94 | const startTime = Date.now();
95 | try {
96 | const query = `SELECT c.id, c.type, c.brand, c.name, c.description, c.price FROM c`;
97 | const sqlQuerySpec: SqlQuerySpec = {
98 | query: query,
99 | };
100 | const { resources: items } = await this.productsContainer.items.query(sqlQuerySpec).fetchAll();
101 |
102 | const duration = Date.now() - startTime;
103 | console.debug(`[${sourceFile}] getProducts: ${duration} ms`);
104 |
105 | return {
106 | data: items,
107 | statusCode: 200,
108 | duration: duration,
109 | };
110 | } catch (error) {
111 | return {
112 | error: "Error fetching products from Cosmos DB: " + error,
113 | statusCode: 500,
114 | };
115 | }
116 | }
117 |
118 | async createOrder(cart: Cart) {
119 | const startTime = Date.now();
120 | try {
121 | const order = {
122 | id: Date.now().toString(),
123 | items: cart.items,
124 | userName: cart.userName,
125 | total: cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
126 | status: "completed",
127 | createdAt: new Date().toISOString(),
128 | };
129 |
130 | const { resource } = await this.ordersContainer.items.create(order);
131 |
132 | const duration = Date.now() - startTime;
133 | console.debug(`[${sourceFile}] createOrder: ${duration} ms`);
134 |
135 | return {
136 | data: resource,
137 | statusCode: 200,
138 | duration: duration,
139 | };
140 | } catch (error) {
141 | return {
142 | error: "Error creating order in Cosmos DB: " + error,
143 | statusCode: 500,
144 | };
145 | }
146 | }
147 | }
148 |
149 | // Create a singleton instance
150 | export const cosmosDBService = new CosmosDBService();
151 |
```
--------------------------------------------------------------------------------
/nextjs/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
5 |
6 | import { cn } from '@/lib/utils';
7 | import { buttonVariants } from '@/components/ui/button';
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root;
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal;
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
17 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
18 | >(({ className, ...props }, ref) => (
19 | <AlertDialogPrimitive.Overlay
20 | className={cn(
21 | 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
22 | className
23 | )}
24 | {...props}
25 | ref={ref}
26 | />
27 | ));
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef<typeof AlertDialogPrimitive.Content>,
32 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33 | >(({ className, ...props }, ref) => (
34 | <AlertDialogPortal>
35 | <AlertDialogOverlay />
36 | <AlertDialogPrimitive.Content
37 | ref={ref}
38 | className={cn(
39 | 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
40 | className
41 | )}
42 | {...props}
43 | />
44 | </AlertDialogPortal>
45 | ));
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes<HTMLDivElement>) => (
52 | <div
53 | className={cn(
54 | 'flex flex-col space-y-2 text-center sm:text-left',
55 | className
56 | )}
57 | {...props}
58 | />
59 | );
60 | AlertDialogHeader.displayName = 'AlertDialogHeader';
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes<HTMLDivElement>) => (
66 | <div
67 | className={cn(
68 | 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
69 | className
70 | )}
71 | {...props}
72 | />
73 | );
74 | AlertDialogFooter.displayName = 'AlertDialogFooter';
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef<typeof AlertDialogPrimitive.Title>,
78 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
79 | >(({ className, ...props }, ref) => (
80 | <AlertDialogPrimitive.Title
81 | ref={ref}
82 | className={cn('text-lg font-semibold', className)}
83 | {...props}
84 | />
85 | ));
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef<typeof AlertDialogPrimitive.Description>,
90 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
91 | >(({ className, ...props }, ref) => (
92 | <AlertDialogPrimitive.Description
93 | ref={ref}
94 | className={cn('text-sm text-muted-foreground', className)}
95 | {...props}
96 | />
97 | ));
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName;
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef<typeof AlertDialogPrimitive.Action>,
103 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
104 | >(({ className, ...props }, ref) => (
105 | <AlertDialogPrimitive.Action
106 | ref={ref}
107 | className={cn(buttonVariants(), className)}
108 | {...props}
109 | />
110 | ));
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
115 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
116 | >(({ className, ...props }, ref) => (
117 | <AlertDialogPrimitive.Cancel
118 | ref={ref}
119 | className={cn(
120 | buttonVariants({ variant: 'outline' }),
121 | 'mt-2 sm:mt-0',
122 | className
123 | )}
124 | {...props}
125 | />
126 | ));
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | };
142 |
```