├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .env.sample ├── .gitattributes ├── .github └── workflows │ └── azure-dev.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── _docs ├── session-recommender-architecture.png └── session-recommender.png ├── _tests └── auth.http ├── api ├── .gitignore ├── .vscode │ └── extensions.json ├── AuthenticationProcessor.cs ├── Properties │ └── launchSettings.json ├── api.csproj ├── host.json └── notes.txt ├── azure.yaml ├── client ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── index.css │ ├── main.jsx │ ├── routes │ │ ├── root.jsx │ │ └── sessionsList.jsx │ ├── sessions.js │ └── user.js ├── staticwebapp.config.json ├── staticwebapp.config.json.sample └── vite.config.js ├── database ├── SessionRecommender │ ├── Script.PostDeployment.sql │ ├── Script.PreDeployment.sql │ ├── Security │ │ ├── session_recommender_app.sql │ │ ├── session_recommender_app__db_owner.sql │ │ └── web.sql │ ├── SessionRecommender.sqlproj │ └── web │ │ ├── Sequences │ │ └── global_id.sql │ │ ├── StoredProcedures │ │ ├── find_sessions.sql │ │ ├── get_sessions_count.sql │ │ └── upsert_session_abstract_embeddings.sql │ │ └── Tables │ │ ├── searched_text.sql │ │ ├── session_abstract_embeddings.sql │ │ ├── sessions.sql │ │ ├── user_session_favorites.sql │ │ └── users.sql ├── setup-database.sh └── setup-database.sql ├── func ├── .gitignore ├── Properties │ └── launchSettings.json ├── SessionProcessor.cs ├── SessionProcessor.csproj ├── host.json └── local.settings.json.sample ├── infra ├── abbreviations.json ├── app │ ├── functions.bicep │ ├── openai.bicep │ ├── sqlserver.bicep │ └── staticwebapp.bicep ├── core │ ├── host │ │ ├── appservice-appsettings.bicep │ │ ├── appservice.bicep │ │ ├── appserviceplan.bicep │ │ └── functions.bicep │ ├── monitor │ │ ├── applicationinsights-dashboard.bicep │ │ ├── applicationinsights.bicep │ │ └── loganalytics.bicep │ ├── security │ │ ├── keyvault-access.bicep │ │ ├── keyvault.bicep │ │ └── role.bicep │ └── storage │ │ └── storage-account.bicep ├── main.bicep └── main.parameters.json ├── swa-cli.config.json └── swa-db-connections └── staticwebapp.database.config.json /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE=bullseye 2 | FROM --platform=amd64 mcr.microsoft.com/devcontainers/${IMAGE} 3 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \ 4 | && mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \ 5 | && sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/debian/$(lsb_release -rs | cut -d'.' -f 1)/prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' \ 6 | && apt-get update && apt-get install -y azure-functions-core-tools-4 \ 7 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Developer CLI", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "IMAGE": "dotnet:7.0-bullseye" 7 | } 8 | }, 9 | "features": { 10 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 11 | }, 12 | "ghcr.io/devcontainers/features/node:1": { 13 | "version": "18", 14 | "nodeGypDependencies": false 15 | }, 16 | "ghcr.io/azure/azure-dev/azd:latest": {} 17 | }, 18 | "customizations": { 19 | "vscode": { 20 | "extensions": [ 21 | "GitHub.vscode-github-actions", 22 | "ms-azuretools.azure-dev", 23 | "ms-azuretools.vscode-azurefunctions", 24 | "ms-azuretools.vscode-bicep", 25 | "ms-azuretools.vscode-docker", 26 | "ms-dotnettools.csharp", 27 | "ms-dotnettools.vscode-dotnet-runtime", 28 | "ms-vscode.vscode-node-azure-pack" 29 | ] 30 | } 31 | }, 32 | "postCreateCommand": "", 33 | "remoteUser": "vscode", 34 | "hostRequirements": { 35 | "memory": "8gb" 36 | } 37 | } -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | MSSQL='Server=.database.windows.net;Initial Catalog=;Persist Security Info=False;User ID=session_recommender_app;Password=unEno!h5!&*KP420xds&@P901afb$^M;MultipleActiveResultSets=False;Encrypt=True;Connection Timeout=30;' 2 | OPENAI.URL='https://.openai.azure.com' 3 | OPENAI.KEY='' 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Thanks to: https://rehansaeed.com/gitattributes-best-practices/ 2 | 3 | # Set default behavior to automatically normalize line endings. 4 | * text=auto 5 | 6 | # Force batch scripts to always use CRLF line endings so that if a repo is accessed 7 | # in Windows via a file share from Linux, the scripts will work. 8 | *.{cmd,[cC][mM][dD]} text eol=crlf 9 | *.{bat,[bB][aA][tT]} text eol=crlf 10 | 11 | # Force bash scripts to always use LF line endings so that if a repo is accessed 12 | # in Unix via a file share from Windows, the scripts will work. 13 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | # Run when commits are pushed to mainline branch (main or master) 5 | # Set this to the mainline branch you are using 6 | branches: 7 | - main 8 | - master 9 | 10 | # GitHub Actions workflow to deploy to Azure using azd 11 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config` 12 | 13 | # Set up permissions for deploying with secretless Azure federated credentials 14 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 15 | permissions: 16 | id-token: write 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | env: 23 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 24 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 25 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 26 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | 31 | - name: Install azd 32 | uses: Azure/setup-azd@v0.1.0 33 | 34 | - name: Install Nodejs 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: 18 38 | 39 | - name: Log in with Azure (Federated Credentials) 40 | if: ${{ env.AZURE_CLIENT_ID != '' }} 41 | run: | 42 | azd auth login ` 43 | --client-id "$Env:AZURE_CLIENT_ID" ` 44 | --federated-credential-provider "github" ` 45 | --tenant-id "$Env:AZURE_TENANT_ID" 46 | shell: pwsh 47 | 48 | - name: Log in with Azure (Client Credentials) 49 | if: ${{ env.AZURE_CREDENTIALS != '' }} 50 | run: | 51 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 52 | Write-Host "::add-mask::$($info.clientSecret)" 53 | 54 | azd auth login ` 55 | --client-id "$($info.clientId)" ` 56 | --client-secret "$($info.clientSecret)" ` 57 | --tenant-id "$($info.tenantId)" 58 | shell: pwsh 59 | env: 60 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 61 | 62 | - name: Provision Infrastructure 63 | run: azd provision --no-prompt 64 | env: 65 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 66 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 67 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 68 | 69 | - name: Deploy Application 70 | run: azd deploy --no-prompt 71 | env: 72 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 73 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 74 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | # AZD 401 | .azure 402 | 403 | # Custom 404 | .env 405 | azuredeploy.parameters.json 406 | *.zip 407 | 408 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-dotnettools.csharp" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "func/bin/Release/net6.0/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~4", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish (functions)", 7 | "azureFunctions.projectSubpath": "func" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean (functions)", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile", 14 | "options": { 15 | "cwd": "${workspaceFolder}/func" 16 | } 17 | }, 18 | { 19 | "label": "build (functions)", 20 | "command": "dotnet", 21 | "args": [ 22 | "build", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "type": "process", 27 | "dependsOn": "clean (functions)", 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile", 33 | "options": { 34 | "cwd": "${workspaceFolder}/func" 35 | } 36 | }, 37 | { 38 | "label": "clean release (functions)", 39 | "command": "dotnet", 40 | "args": [ 41 | "clean", 42 | "--configuration", 43 | "Release", 44 | "/property:GenerateFullPaths=true", 45 | "/consoleloggerparameters:NoSummary" 46 | ], 47 | "type": "process", 48 | "problemMatcher": "$msCompile", 49 | "options": { 50 | "cwd": "${workspaceFolder}/func" 51 | } 52 | }, 53 | { 54 | "label": "publish (functions)", 55 | "command": "dotnet", 56 | "args": [ 57 | "publish", 58 | "--configuration", 59 | "Release", 60 | "/property:GenerateFullPaths=true", 61 | "/consoleloggerparameters:NoSummary" 62 | ], 63 | "type": "process", 64 | "dependsOn": "clean release (functions)", 65 | "problemMatcher": "$msCompile", 66 | "options": { 67 | "cwd": "${workspaceFolder}/func" 68 | } 69 | }, 70 | { 71 | "type": "func", 72 | "dependsOn": "build (functions)", 73 | "options": { 74 | "cwd": "${workspaceFolder}/func/bin/Debug/net6.0" 75 | }, 76 | "command": "host start", 77 | "isBackground": true, 78 | "problemMatcher": "$func-dotnet-watch" 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Davide Mauri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - azdeveloper 5 | - csharp 6 | - sql 7 | - tsql 8 | - javascript 9 | - html 10 | - bicep 11 | products: 12 | - azure-functions 13 | - azure-sql-database 14 | - static-web-apps 15 | - sql-server 16 | - azure-sql-managed-instance 17 | - azure-sqlserver-vm 18 | - dotnet 19 | - azure-openai 20 | urlFragment: azure-sql-db-session-recommender 21 | name: Session Recommender using Azure SQL DB, Open AI and Vector Search 22 | description: Build a session recommender using Jamstack and Event-Driven architecture, using Azure SQL DB to store and search vectors embeddings generated using OpenAI 23 | --- 24 | 25 | 26 | # Session Recommender Sample 27 | 28 | ![Architecture Diagram](./_docs/session-recommender-architecture.png) 29 | 30 | A session recommender built using 31 | 32 | - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/overview) 33 | - [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/) 34 | - [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview?pivots=programming-language-csharp) 35 | - [Azure SQL Database](https://www.sqlservercentral.com/articles/the-sql-developer-experience-beyond-rdbms) 36 | - [Data API builder](https://aka.ms/dab) 37 | 38 | For more details on the solution check also the following articles: 39 | 40 | - [How I built a session recommender in 1 hour using Open AI](https://dev.to/azure/how-i-built-a-session-recommender-in-1-hour-using-open-ai-5419) 41 | - [Vector Similarity Search with Azure SQL database and OpenAI](https://devblogs.microsoft.com/azure-sql/vector-similarity-search-with-azure-sql-database-and-openai/) 42 | 43 | ## Retrieval Augmented Generation (RAG) 44 | 45 | An enhanced version of this sample, that also include Retrieval Augmented Generation (RAG), is available at this repository: https://github.com/Azure-Samples/azure-sql-db-session-recommender-v2. If you are new to similarity search and RAG, it is recommended to start with this repo and then move to the enhanced one. 46 | 47 | # Deploy the sample using the Azure Developer CLI (azd) template 48 | 49 | The Azure Developer CLI (`azd`) is a developer-centric command-line interface (CLI) tool for creating Azure applications. 50 | 51 | ## Prerequisites 52 | 53 | - Install [AZD CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd). 54 | - Install [.NET SDK](https://dotnet.microsoft.com/download). 55 | - Install [Node.js](https://nodejs.org/download/). 56 | - Install [SWA CLI](https://azure.github.io/static-web-apps-cli/docs/use/install#installing-the-cli). 57 | 58 | ## Install AZD CLI 59 | 60 | You need to install it before running and deploying with the Azure Developer CLI. 61 | 62 | ### Windows 63 | 64 | ```powershell 65 | powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" 66 | ``` 67 | 68 | ### Linux/MacOS 69 | 70 | ```bash 71 | curl -fsSL https://aka.ms/install-azd.sh | bash 72 | ``` 73 | 74 | After logging in with the following command, you will be able to use azd cli to quickly provision and deploy the application. 75 | 76 | ## Authenticate with Azure 77 | 78 | Make sure AZD CLI can access Azure resources. You can use the following command to log in to Azure: 79 | 80 | ```bash 81 | azd auth login 82 | ``` 83 | 84 | ## Initialize the template 85 | 86 | Then, execute the `azd init` command to initialize the environment (You do not need to run this command if you already have the code or have opened this in a Codespace or DevContainer). 87 | 88 | ```bash 89 | azd init -t Azure-Samples/azure-sql-db-session-recommender 90 | ``` 91 | 92 | Enter an environment name. 93 | 94 | ## Deploy the sample 95 | 96 | Run `azd up` to provision all the resources to Azure and deploy the code to those resources. 97 | 98 | ```bash 99 | azd up 100 | ``` 101 | 102 | Select your desired `subscription` and `location`. Then choose a resource group or create a new resource group. Wait a moment for the resource deployment to complete, click the Website endpoint and you will see the web app page. 103 | 104 | **Note**: Make sure to pick a region where all services are available like, for example, *West Europe* or *East US 2* 105 | 106 | ## GitHub Actions 107 | 108 | Using the Azure Developer CLI, you can setup your pipelines, monitor your application, test and debug locally. 109 | 110 | ```bash 111 | azd pipeline config 112 | ``` 113 | 114 | ## Test the solution 115 | 116 | Add a new row to the `Sessions` table using the following SQL statement (you can use tools like [Azure Data Studio](https://learn.microsoft.com/en-us/azure-data-studio/quickstart-sql-database) or [SQL Server Management Studio](https://learn.microsoft.com/en-us/azure/azure-sql/database/connect-query-ssms?view=azuresql) to connect to the database. No need to install them if you don't want. In that case you can use the [SQL Editor in the Azure Portal](https://learn.microsoft.com/en-us/azure/azure-sql/database/connect-query-portal?view=azuresql)): 117 | 118 | ```sql 119 | insert into web.sessions 120 | (title, abstract) 121 | values 122 | ('Building a session recommender using OpenAI and Azure SQL', 'In this fun and demo-driven session you’ll learn how to integrate Azure SQL with OpenAI to generate text embeddings, store them in the database, index them and calculate cosine distance to build a session recommender. And once that is done, you’ll publish it as a REST and GraphQL API to be consumed by a modern JavaScript frontend. Sounds pretty cool, uh? Well, it is!') 123 | ``` 124 | 125 | immediately the deployed Azure Function will get executed in response to the `INSERT` statement. The Azure Function will call the OpenAI service to generate the text embedding for the session title and abstract, and then store the embedding in the database, specifically in the `web.session_abstract_embeddings` table. 126 | 127 | ```sql 128 | select * from web.session_abstract_embeddings 129 | ``` 130 | 131 | You can now open the URL associated with the created Static Web App to see the session recommender in action. You can get the URL from the Static Web App overview page in the Azure portal. 132 | 133 | ![Website running](./_docs/session-recommender.png) 134 | 135 | ## Run the solution locally 136 | 137 | The whole solution can be executed locally, using [Static Web App CLI](https://github.com/Azure/static-web-apps-cli) and [Azure Function CLI](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-csharp). 138 | 139 | Install the required node packages needed by the fronted: 140 | 141 | ```bash 142 | cd client 143 | npm install 144 | ``` 145 | 146 | once finished, create a `./func/local.settings.json` and `.env` starting from provided samples files, and fill out the settings using the correct values for your environment. 147 | 148 | From the sample root folder run: 149 | 150 | ```bash 151 | swa start --app-location ./client --data-api-location ./swa-db-connections/ 152 | ``` 153 | 154 | ## (Optional) Use a custom authentication provider with Static Web Apps 155 | 156 | The folder `api` contains a sample function to customize the authentication process as described in the [Custom authentication in Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/authentication-custom?tabs=aad%2Cinvitations#configure-a-custom-identity-provider) article. The function will add any user with a `@microsoft.com` to the `microsoft` role. Data API builder can be configured to allow acceess to a certain API only to users with a certain role, for example: 157 | 158 | ```json 159 | "permissions": [ 160 | { 161 | "role": "microsoft", 162 | "actions": [{ 163 | "action": "execute" 164 | }] 165 | } 166 | ] 167 | ``` 168 | 169 | This step is optional and is provided mainly as an example on how to use custom authentication with SWA and DAB. It is not used in the solution. 170 | -------------------------------------------------------------------------------- /_docs/session-recommender-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-sql-db-session-recommender/05f0291d06569ff8e727fc86c94f569b088bde60/_docs/session-recommender-architecture.png -------------------------------------------------------------------------------- /_docs/session-recommender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-sql-db-session-recommender/05f0291d06569ff8e727fc86c94f569b088bde60/_docs/session-recommender.png -------------------------------------------------------------------------------- /_tests/auth.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:7071/api/AuthenticationProcessor -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /api/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } -------------------------------------------------------------------------------- /api/AuthenticationProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.Azure.WebJobs.Extensions.Http; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | using System.Collections.Generic; 11 | 12 | namespace api 13 | { 14 | public static class AuthenticationProcessor 15 | { 16 | [FunctionName("AuthenticationProcessor")] 17 | public static async Task Run( 18 | [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, 19 | ILogger log) 20 | { 21 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 22 | dynamic data = JsonConvert.DeserializeObject(requestBody); 23 | 24 | string userDetails = data?.userDetails ?? string.Empty; 25 | 26 | var roles = new List();; 27 | 28 | if (userDetails.EndsWith("@microsoft.com")) 29 | roles.Add("microsoft"); 30 | 31 | log.LogInformation($"User {userDetails} has roles {string.Join(",", roles)}"); 32 | 33 | return new OkObjectResult(new { roles }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "api": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--port 7146", 6 | "launchBrowser": false 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /api/api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | v4 5 | 6 | 7 | 8 | 9 | 10 | 11 | PreserveNewest 12 | 13 | 14 | PreserveNewest 15 | Never 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | }, 9 | "enableLiveMetricsFilters": true 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /api/notes.txt: -------------------------------------------------------------------------------- 1 | SWA -> STANDARD 2 | Create Entra ID "Register Enterprise App" 3 | Add App in certificates / secrects 4 | Enable "ID Tokens" in Authentication 5 | Add Web platform with url: 6 | https://.azurestaticapps.net/.auth/login/aad/callback 7 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: azure-sql-db-session-recommender 4 | metadata: 5 | template: azure-sql-db-session-recommender 6 | services: 7 | web: 8 | project: ./client 9 | language: js 10 | host: staticwebapp 11 | dist: dist 12 | functionapp: 13 | project: ./func 14 | language: dotnet 15 | host: function -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenAI Powered Session Recommender 7 | 8 | 9 | 10 | 24 | 25 | 26 | 27 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "localforage": "^1.10.0", 14 | "localstorage-slim": "^2.7.0", 15 | "match-sorter": "^6.3.1", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.16.0", 19 | "sort-by": "^1.2.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.2.15", 23 | "@types/react-dom": "^18.2.7", 24 | "@vitejs/plugin-react": "^4.0.3", 25 | "eslint": "^8.45.0", 26 | "eslint-plugin-react": "^7.32.2", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.3", 29 | "vite": "^4.4.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | html:focus, 9 | html:focus-within { 10 | scroll-behavior: smooth; 11 | } 12 | 13 | body { 14 | -webkit-font-smoothing: antialiased; 15 | } 16 | 17 | img, 18 | picture, 19 | video, 20 | canvas, 21 | svg { 22 | display: block; 23 | max-width: 100%; 24 | } 25 | 26 | input, 27 | button, 28 | textarea, 29 | select { 30 | font: inherit; 31 | } 32 | 33 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ 34 | @media (prefers-reduced-motion: reduce) { 35 | html:focus, 36 | html:focus-within { 37 | scroll-behavior: auto; 38 | } 39 | 40 | *, 41 | *::before, 42 | *::after { 43 | animation-duration: 0.01ms !important; 44 | animation-iteration-count: 1 !important; 45 | transition-duration: 0.01ms !important; 46 | scroll-behavior: auto !important; 47 | } 48 | } 49 | 50 | /* Design tokens */ 51 | :root { 52 | --color-midnight-blue-primary: #190649ff; 53 | --color-midnight-blue-shade-10: #14053aff; 54 | --color-midnight-blue-shade-20: #0f042cff; 55 | --color-brand-purple-primary: #512bd4ff; 56 | --color-brand-purple-shade-10: #4122aaff; 57 | --color-brand-purple-shade-20: #311a7fff; 58 | --color-brand-purple-shade-30: #201155ff; 59 | --color-brand-purple-shade-35: #180d40ff; 60 | --color-brand-purple-shade-40: #10092aff; 61 | --color-brand-purple-shade-45: #080415ff; 62 | --color-brand-purple-tint-10: #7455ddff; 63 | --color-brand-purple-tint-20: #9780e5ff; 64 | --color-brand-purple-tint-25: #ac99eaff; 65 | --color-brand-purple-tint-30: #b9aaeeff; 66 | --color-brand-purple-tint-35: #cbbff2ff; 67 | --color-brand-purple-tint-40: #dcd5f6ff; 68 | --color-brand-purple-tint-45: #eeeafbff; 69 | --color-cyan-primary: #28c2d1ff; 70 | --color-cyan-shade-10: #209ba7ff; 71 | --color-cyan-shade-20: #18747dff; 72 | --color-cyan-shade-30: #104e54ff; 73 | --color-cyan-shade-35: #0c3a3fff; 74 | --color-cyan-shade-40: #08272aff; 75 | --color-cyan-shade-45: #041315ff; 76 | --color-cyan-tint-10: #53cedaff; 77 | --color-cyan-tint-20: #7edae3ff; 78 | --color-cyan-tint-30: #a9e7edff; 79 | --color-cyan-tint-35: #beedf1ff; 80 | --color-cyan-tint-40: #d4f3f6ff; 81 | --color-cyan-tint-45: #e9f9faff; 82 | --color-blue-primary: #0b6cffff; 83 | --color-blue-shade-10: #0956ccff; 84 | --color-blue-shade-20: #074199ff; 85 | --color-blue-shade-30: #042b66ff; 86 | --color-blue-shade-35: #03204dff; 87 | --color-blue-shade-40: #021633ff; 88 | --color-blue-shade-45: #010b1aff; 89 | --color-blue-tint-10: #3c89ffff; 90 | --color-blue-tint-20: #6da7ffff; 91 | --color-blue-tint-30: #9dc4ffff; 92 | --color-blue-tint-35: #b6d3ffff; 93 | --color-blue-tint-40: #cee2ffff; 94 | --color-blue-tint-45: #e7f0ffff; 95 | --color-flamingo-primary: #f65163ff; 96 | --color-flamingo-shade-10: #cb4150ff; 97 | --color-flamingo-shade-20: #a0313dff; 98 | --color-flamingo-shade-30: #74222bff; 99 | --color-flamingo-shade-35: #5f1a21ff; 100 | --color-flamingo-shade-40: #491218ff; 101 | --color-flamingo-shade-45: #340a0eff; 102 | --color-flamingo-tint-10: #f87482ff; 103 | --color-flamingo-tint-20: #fa97a1ff; 104 | --color-flamingo-tint-30: #fbb9c1ff; 105 | --color-flamingo-tint-35: #fccbd0ff; 106 | --color-flamingo-tint-40: #fddce0ff; 107 | --color-flamingo-tint-45: #feeeefff; 108 | --color-magenta-primary: #d600aaff; 109 | --color-magenta-shade-10: #ab0088ff; 110 | --color-magenta-shade-20: #800066ff; 111 | --color-magenta-shade-30: #560044ff; 112 | --color-magenta-shade-35: #400033ff; 113 | --color-magenta-shade-40: #2b0022ff; 114 | --color-magenta-shade-45: #150011ff; 115 | --color-magenta-tint-10: #de33bbff; 116 | --color-magenta-tint-20: #e666ccff; 117 | --color-magenta-tint-30: #ef99ddff; 118 | --color-magenta-tint-35: #f3b2e5ff; 119 | --color-magenta-tint-40: #f7cceeff; 120 | --color-magenta-tint-45: #fbe5f6ff; 121 | --color-yellow-primary: #f7b548ff; 122 | --color-yellow-shade-10: #ca943aff; 123 | --color-yellow-shade-20: #9d722cff; 124 | --color-yellow-shade-30: #70511dff; 125 | --color-yellow-shade-35: #5a4016ff; 126 | --color-yellow-shade-40: #432f0fff; 127 | --color-yellow-shade-45: #2d1f08ff; 128 | --color-yellow-tint-10: #f9c46dff; 129 | --color-yellow-tint-20: #fad391ff; 130 | --color-yellow-tint-30: #fce1b6ff; 131 | --color-yellow-tint-35: #fde9c8ff; 132 | --color-yellow-tint-40: #fdf0daff; 133 | --color-yellow-tint-46: #fef8edff; 134 | --color-neutrals-dark-black: #000000ff; 135 | --color-neutrals-dark-grey-4: #0a0a0aff; 136 | --color-neutrals-dark-grey-8: #141414ff; 137 | --color-neutrals-dark-grey-24: #3d3d3dff; 138 | --color-neutrals-dark-grey-36: #5c5c5cff; 139 | --color-neutrals-dark-grey-40: #666666ff; 140 | --color-neutrals-dark-grey-52: #858585ff; 141 | --color-neutrals-dark-grey-68: #adadadff; 142 | --color-neutrals-dark-grey-84: #d6d6d6ff; 143 | --color-neutrals-additionals-grey-2: #050505ff; 144 | --color-neutrals-additionals-grey-6: #0f0f0fff; 145 | --color-neutrals-additionals-grey-10: #1a1a1aff; 146 | --color-neutrals-additionals-grey-12: #1f1f1fff; 147 | --color-neutrals-additionals-grey-16: #292929ff; 148 | --color-neutrals-additionals-grey-18: #2e2e2eff; 149 | --color-neutrals-additionals-grey-20: #333333ff; 150 | --color-neutrals-additionals-grey-22: #383838ff; 151 | --color-neutrals-additionals-grey-28: #474747ff; 152 | --color-neutrals-additionals-grey-30: #4d4d4dff; 153 | --color-neutrals-additionals-grey-32: #525252ff; 154 | --color-neutrals-additionals-grey-34: #575757ff; 155 | --color-neutrals-additionals-grey-42: #6b6b6bff; 156 | --color-neutrals-additionals-grey-44: #707070ff; 157 | --color-neutrals-additionals-grey-46: #757575ff; 158 | --color-neutrals-additionals-grey-48: #7a7a7aff; 159 | --color-neutrals-additionals-grey-54: #8a8a8aff; 160 | --color-neutrals-additionals-grey-56: #8f8f8fff; 161 | --color-neutrals-additionals-grey-58: #949494ff; 162 | --color-neutrals-additionals-grey-60: #999999ff; 163 | --color-neutrals-additionals-grey-62: #9e9e9eff; 164 | --color-neutrals-additionals-grey-64: #a3a3a3ff; 165 | --color-neutrals-additionals-grey-66: #a8a8a8ff; 166 | --color-neutrals-additionals-grey-70: #b2b2b2ff; 167 | --color-neutrals-additionals-grey-72: #b8b8b8ff; 168 | --color-neutrals-additionals-grey-76: #c2c2c2ff; 169 | --color-neutrals-additionals-grey-78: #c7c7c7ff; 170 | --color-neutrals-additionals-grey-80: #ccccccff; 171 | --color-neutrals-additionals-grey-86: #dbdbdbff; 172 | --color-neutrals-additionals-grey-90: #e5e5e5ff; 173 | --color-neutrals-light-grey-14: #242424ff; 174 | --color-neutrals-light-grey-26: #424242ff; 175 | --color-neutrals-light-grey-38: #616161ff; 176 | --color-neutrals-light-grey-50: #808080ff; 177 | --color-neutrals-light-grey-74: #bdbdbdff; 178 | --color-neutrals-light-grey-82: #d1d1d1ff; 179 | --color-neutrals-light-grey-88: #e0e0e0ff; 180 | --color-neutrals-light-grey-90: #ebebebff; 181 | --color-neutrals-light-grey-94: #f0f0f0ff; 182 | --color-neutrals-light-grey-96: #f5f5f5ff; 183 | --color-neutrals-light-grey-98: #fafafaff; 184 | --color-neutrals-light-white: #ffffffff; 185 | --color-information-blue: #005fb7ff; 186 | --color-information-dark-green: #0f7b0fff; 187 | --color-information-light-green: #dff6ddff; 188 | --color-information-dark-red: #c42b1cff; 189 | --color-information-light-red: #fde7e9ff; 190 | --color-information-dark-green: #052505ff; 191 | --color-information-dark-red: #3f1011ff; 192 | --color-gradients-light-theme-multi-color-gradients-purple-magenta: linear-gradient(to bottom, #512bd451 0%, #d600aad6 100%); 193 | --color-gradients-light-theme-multi-color-gradients-magenta-flamingo: linear-gradient(to bottom, #d600aad6 0%, #cb4150cb 100%); 194 | --color-gradients-light-theme-multi-color-gradients-magenta-flamingo-webkit: -webkit-linear-gradient(to bottom, #d600aad6 0%, #cb4150cb 100%); 195 | --color-gradients-light-theme-multi-color-gradients-flamingo-purple: linear-gradient(to bottom, #cb4150cb 0%, #512bd451 100%); 196 | --color-gradients-light-theme-multi-color-gradients-purple-blue: linear-gradient(to bottom, #512bd451 0%, #0b6cff0b 100%); 197 | --color-gradients-light-theme-multi-color-gradients-blue-purple: linear-gradient(to bottom, #0b6cff0b 0%, #512bd451 100%); 198 | --color-gradients-light-theme-magenta-purple-primary: linear-gradient(to bottom, #d600aad6 0%, #512bd451 100%); 199 | --color-gradients-light-theme-magenta-purple-primary-webkit: -webkit-linear-gradient(to bottom, #d600aad6 0%, #512bd451 100%); 200 | --color-gradients-light-theme-magenta-purple-gradient-01: linear-gradient(to bottom, #d600aad6 0%, #b80ab3b8 85.4%); 201 | --color-gradients-light-theme-magenta-purple-gradient-02: linear-gradient(to bottom, #b80ab3b8 0%, #9b13bd9b 100%); 202 | --color-gradients-light-theme-magenta-purple-gradient-03: linear-gradient(to bottom, #9b13bd9b 0%, #7d1dc67d 100%); 203 | --color-gradients-light-theme-magenta-purple-gradient-04: linear-gradient(to bottom, #7d1dc67d 0%, #512bd451 100%); 204 | --color-gradients-light-theme-magenta-purple-gradient-05: linear-gradient(to bottom, #512bd451 0%, #7d1dc67d 100%); 205 | --color-gradients-light-theme-magenta-purple-gradient-06: linear-gradient(to bottom, #7d1dc67d 0%, #9b13bd9b 100%); 206 | --color-gradients-light-theme-magenta-purple-gradient-07: linear-gradient(to bottom, #9b13bd9b 0%, #b80ab3b8 100%); 207 | --color-gradients-light-theme-magenta-purple-gradient-08: linear-gradient(to bottom, #b80ab3b8 0%, #d600aad6 100%); 208 | --color-gradients-light-theme-blue-purple-primary: linear-gradient(to bottom, #0b6cff0b 0%, #512bd451 100%); 209 | --color-gradients-light-theme-blue-purple-gradient-01: linear-gradient(to bottom, #0b6cff0b 0%, #1c5cf41c 85.4%); 210 | --color-gradients-light-theme-blue-purple-gradient-02: linear-gradient(to bottom, #1c5cf41c 0%, #2e4cea2e 100%); 211 | --color-gradients-light-theme-blue-purple-gradient-03: linear-gradient(to bottom, #2e4cea2e 0%, #403bdf40 100%); 212 | --color-gradients-light-theme-blue-purple-gradient-04: linear-gradient(to bottom, #403bdf40 0%, #512bd451 100%, #512bd451 100%); 213 | --color-gradients-light-theme-blue-purple-gradient-05: linear-gradient(to bottom, #512bd451 0%, #403bdf40 100%); 214 | --color-gradients-light-theme-blue-purple-gradient-06: linear-gradient(to bottom, #403bdf40 0%, #2e4cea2e 100%); 215 | --color-gradients-light-theme-blue-purple-gradient-07: linear-gradient(to bottom, #2e4cea2e 0%, #1c5cf41c 100%); 216 | --color-gradients-light-theme-blue-purple-gradient-08: linear-gradient(to bottom, #1c5cf41c 0%, #0b6cff0b 100%); 217 | --color-gradients-dark-theme-multi-color-gradients-text-graphic-elements-only-purple-magenta: linear-gradient(to bottom, #9780e597 0%, #de33bbde 100%); 218 | --color-gradients-dark-theme-multi-color-gradients-text-graphic-elements-only-purple-magenta-webkit: -webkit-linear-gradient(to bottom, #9780e597 0%, #de33bbde 100%); 219 | --color-gradients-dark-theme-multi-color-gradients-text-graphic-elements-only-magenta-flamingo: linear-gradient(to bottom, #de33bbde 0%, #f65163f6 100%); 220 | --color-gradients-dark-theme-multi-color-gradients-text-graphic-elements-only-flamingo-purple: linear-gradient(to bottom, #f65163f6 0%, #9780e597 100%); 221 | --color-gradients-dark-theme-multi-color-gradients-text-graphic-elements-only-purple-cyan: linear-gradient(to bottom, #9780e597 0%, #53ceda53 100%); 222 | --color-gradients-dark-theme-multi-color-gradients-text-graphic-elements-only-cyan-purple: linear-gradient(to bottom, #53ceda53 0%, #9780e597 100%); 223 | --color-gradients-dark-theme-magenta-purple-primary: linear-gradient(to bottom, #d600aad6 0%, #7455dd74 100%); 224 | --color-gradients-dark-theme-magenta-purple-gradient-02: linear-gradient(to bottom, #be15b7be 0%, #a52bc4a5 100%); 225 | --color-gradients-dark-theme-magenta-purple-gradient-01: linear-gradient(to bottom, #d600aad6 0%, #be15b7be 85.4%); 226 | --color-gradients-dark-theme-magenta-purple-gradient-03: linear-gradient(to bottom, #a52bc4a5 0%, #8d40d08d 100%); 227 | --color-gradients-dark-theme-magenta-purple-gradient-04: linear-gradient(to bottom, #8d40d08d 0%, #7455dd74 100%); 228 | --color-gradients-dark-theme-magenta-purple-gradient-05: linear-gradient(to bottom, #7455dd74 0%, #8d40d08d 100%); 229 | --color-gradients-dark-theme-magenta-purple-gradient-06: linear-gradient(to bottom, #8d40d08d 0%, #a52bc4a5 100%); 230 | --color-gradients-dark-theme-magenta-purple-gradient-07: linear-gradient(to bottom, #a52bc4a5 0%, #be15b7be 100%); 231 | --color-gradients-dark-theme-magenta-purple-gradient-08: linear-gradient(to bottom, #be15b7be 0%, #d600aad6 100%); 232 | --color-gradients-dark-theme-blue-purple-primary: linear-gradient(to bottom, #0b6cff0b 0%, #7455dd74 100%); 233 | --color-gradients-dark-theme-blue-purple-gradient-01: linear-gradient(to bottom, #0b6cff0b 0%, #2566f625 85.4%); 234 | --color-gradients-dark-theme-blue-purple-gradient-02: linear-gradient(to bottom, #2566f625 0%, #4060ee40 100%); 235 | --color-gradients-dark-theme-blue-purple-gradient-03: linear-gradient(to bottom, #4060ee40 0%, #5b5ae55b 100%); 236 | --color-gradients-dark-theme-blue-purple-gradient-04: linear-gradient(to bottom, #5b5ae55b 0%, #7455dd74 100%); 237 | --color-gradients-dark-theme-blue-purple-gradient-05: linear-gradient(to bottom, #7455dd74 0%, #5b5ae55b 100%); 238 | --color-gradients-dark-theme-blue-purple-gradient-06: linear-gradient(to bottom, #5b5ae55b 0%, #4060ee40 100%); 239 | --color-gradients-dark-theme-blue-purple-gradient-07: linear-gradient(to bottom, #4060ee40 0%, #2566f625 100%); 240 | --color-gradients-dark-theme-blue-purple-gradient-08: linear-gradient(to bottom, #2566f625 0%, #0b6cff0b 100%); 241 | --color-speaker-cards-image-backplate-light-theme: linear-gradient(to bottom, #ffffffff 40.71%, #ffffffff 100%); 242 | --color-speaker-cards-image-backplate-dark-theme: linear-gradient(to bottom, #2e2e2e2e 40.71%, #2e2e2e2e 100%); 243 | --base-font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 244 | --title-font-family: "Space Grotesk", Helvetica, Arial, sans-serif; 245 | --content-width-desktop: 1248px; 246 | --content-width-tablet: 792px; 247 | --content-width-mobile: 300px; 248 | /* This ensures that even when a screen width is exactly the size of a content 249 | break point there is still some padding between the viewport and content. */ 250 | --content-width-padding-desktop: 40px; 251 | --content-width-padding-tablet: 30px; 252 | --content-width-padding-mobile: 20px; 253 | --grid-template-desktop: repeat(12, 1fr); 254 | --grid-template-tablet: repeat(12, 1fr); 255 | --grid-template-mobile: repeat(6, 1fr); 256 | --grid-gap-desktop: 48px; 257 | --grid-gap-tablet: 24px; 258 | --grid-gap-mobile: 12px; 259 | /* This defines the vertical spacing between sections of content on any given page 260 | (and is also used as the row gap for grids) */ 261 | --vertical-gap-desktop: 60px; 262 | --vertical-gap-tablet: 32px; 263 | --vertical-gap-mobile: 32px; 264 | --corner-radius-xs: 8px; 265 | --corner-radius-sm: 12px; 266 | --corner-radius-md: 16px; 267 | --corner-radius-lg: 20px; 268 | --corner-radius-xl: 32px; 269 | /* --outline-corner-radius: calc(outline-offset + child-corner-radius); */ 270 | 271 | --color-foreground: var(--color-neutrals-additionals-grey-20); 272 | --color-background: var(--color-neutrals-light-grey-98); 273 | --color-title-font: var(--color-midnight-blue-primary); 274 | --color-navbar-menu-icon-fill: var(--color-brand-purple-primary); 275 | --color-footer-foreground: var(--color-neutrals-dark-grey-24); 276 | --color-footer-background: var(--color-neutrals-light-grey-90); 277 | --color-primary-gradient: linear-gradient(to bottom right, #512BD4, #D600AA); 278 | --color-primary-gradient-webkit: -webkit-linear-gradient(to bottom right, #512BD4, #D600AA); 279 | --gradient-featured-speaker-card-01: linear-gradient(to bottom, #be15b7be 0%, #a52bc4a5 100%); 280 | --gradient-featured-speaker-card-02: linear-gradient(to bottom, #d600aad6 0%, #be15b7be 85.4%); 281 | --gradient-featured-speaker-card-03: linear-gradient(to bottom, #a52bc4a5 0%, #8d40d08d 100%); 282 | --gradient-featured-speaker-card-04: linear-gradient(to bottom, #8d40d08d 0%, #7455dd74 100%); 283 | --gradient-featured-speaker-card-05: linear-gradient(to bottom, #7455dd74 0%, #8d40d08d 100%); 284 | --gradient-featured-speaker-card-06: linear-gradient(to bottom, #8d40d08d 0%, #a52bc4a5 100%); 285 | --featured-speaker-card-foreground: var(--color-neutrals-light-white); 286 | --color-link-foreground: #115EA3; 287 | --color-link-foreground-hover: #0F548C; 288 | --color-link-foreground-active: #0C3B5E; 289 | --color-cookie-banner-foreground: var(--color-foreground); 290 | --color-cookie-banner-background: var(--color-neutrals-light-white); 291 | --color-cookie-border: var(--color-brand-purple-primary); 292 | --dotnet-btn-corner-radius: var(--corner-radius-xs); 293 | --dotnet-btn-border-width: 2px; 294 | --color-dotnet-btn-focus-outline: var(--color-neutrals-dark-black); 295 | --dotnet-btn-focus-outline-width: 2px; 296 | --dotnet-btn-focus-outline-offset: 1px; 297 | --color-dotnet-solid-btn-accent-foreground: var(--color-neutrals-light-white); 298 | --color-dotnet-solid-btn-accent-background: var(--color-brand-purple-primary); 299 | --color-dotnet-solid-btn-accent-border: var(--color-brand-purple-primary); 300 | --color-dotnet-solid-btn-accent-foreground-hover: var(--color-neutrals-light-white); 301 | --color-dotnet-solid-btn-accent-background-hover: var(--color-brand-purple-shade-10); 302 | --color-dotnet-solid-btn-accent-border-hover: var(--color-brand-purple-shade-10); 303 | --color-dotnet-solid-btn-accent-foreground-active: var(--color-neutrals-light-white); 304 | --color-dotnet-solid-btn-accent-background-active: var(--color-brand-purple-shade-20); 305 | --color-dotnet-solid-btn-accent-border-active: var(--color-brand-purple-shade-20); 306 | --color-dotnet-outline-btn-accent-foreground: var(--color-brand-purple-primary); 307 | --color-dotnet-outline-btn-accent-background: transparent; 308 | --color-dotnet-outline-btn-accent-border: var(--color-brand-purple-primary); 309 | --color-dotnet-outline-btn-accent-foreground-hover: var(--color-brand-purple-shade-10); 310 | --color-dotnet-outline-btn-accent-background-hover: var(--color-brand-purple-tint-45); 311 | --color-dotnet-outline-btn-accent-border-hover: var(--color-brand-purple-primary); 312 | --color-dotnet-outline-btn-accent-foreground-active: var(--color-brand-purple-shade-20); 313 | --color-dotnet-outline-btn-accent-background-active: var(--color-brand-purple-tint-40); 314 | --color-dotnet-outline-btn-accent-border-active: var(--color-brand-purple-primary); 315 | --color-dotnet-link-btn-accent-foreground: var(--color-brand-purple-primary); 316 | --color-dotnet-link-btn-accent-background: transparent; 317 | --color-dotnet-link-btn-accent-border: transparent; 318 | --color-dotnet-link-btn-accent-foreground-hover: var(--color-brand-purple-shade-10); 319 | --color-dotnet-link-btn-accent-background-hover: transparent; 320 | --color-dotnet-link-btn-accent-border-hover: transparent; 321 | --color-dotnet-link-btn-accent-foreground-active: var(--color-brand-purple-shade-20); 322 | --color-dotnet-link-btn-accent-background-active: transparent; 323 | --color-dotnet-link-btn-accent-border-active: transparent; 324 | --color-dotnet-link-btn-neutral-foreground: var(--color-neutrals-dark-grey-24); 325 | --color-dotnet-link-btn-neutral-background: transparent; 326 | --color-dotnet-link-btn-neutral-border: transparent; 327 | --color-dotnet-link-btn-neutral-foreground-hover: var(--color-neutrals-dark-grey-8); 328 | --color-dotnet-link-btn-neutral-background-hover: transparent; 329 | --color-dotnet-link-btn-neutral-border-hover: transparent; 330 | --color-dotnet-link-btn-neutral-foreground-active: var(--color-neutrals-dark-grey-8); 331 | --color-dotnet-link-btn-neutral-background-active: transparent; 332 | --color-dotnet-link-btn-neutral-border-active: transparent; 333 | --color-dotnet-menu-popover-focus-border: var(--color-neutrals-dark-black); 334 | --color-dotnet-menu-popover-foreground: var(--color-neutrals-additionals-grey-28); 335 | --color-dotnet-menu-popover-background: var(--color-neutrals-light-white); 336 | --color-dotnet-menu-popover-foreground-hover: var(--color-neutrals-light-grey-26); 337 | --color-dotnet-menu-popover-background-hover: var(--color-neutrals-light-grey-96); 338 | --color-dotnet-menu-popover-foreground-active: var(--color-neutrals-light-grey-88); 339 | --color-dotnet-menu-popover-background-active: var(--color-neutrals-light-grey-26); 340 | --color-dotnet-content-region-background: var(--color-neutrals-light-white); 341 | --dotnet-speaker-card-foreground: var(--color-neutrals-light-grey-38); 342 | --dotnet-speaker-card-background: var(--color-neutrals-light-white); 343 | --dotnet-speaker-card-box-shadow: 0 2px 10px 4px rgba(0, 0, 0, 0.04); 344 | --dotnet-speaker-card-box-shadow-hover: 0 2px 10px 4px rgba(0, 0, 0, 0.2); 345 | --color-agenda-time: var(--color-neutrals-dark-grey-40); 346 | --color-agenda-summaries: var(--color-neutrals-dark-grey-18); 347 | --color-agenda-speaker: var(--color-brand-purple-shade-20); 348 | --color-local-events-date: var(--color-neutrals-dark-grey-24); 349 | --color-form-input-foreground: var(--color-foreground); 350 | --color-form-input-background: var(--color-neutrals-light-white); 351 | --color-form-input-border: var(--color-neutrals-light-grey-82); 352 | --logo-dark-display: none; 353 | --logo-light-display: block; 354 | } 355 | 356 | /* Dark mode design tokens */ 357 | @media (prefers-color-scheme: dark) { 358 | :root { 359 | --color-foreground: var(--color-neutrals-additionals-grey-90); 360 | --color-background: var(--color-neutrals-additionals-grey-12); 361 | --color-title-font: var(--color-neutrals-light-white); 362 | --color-navbar-menu-icon-fill: var(--color-brand-purple-tint-30); 363 | --color-footer-foreground: var(--color-neutrals-additionals-grey-90); 364 | --color-footer-background: var(--color-neutrals-additionals-grey-18); 365 | --color-primary-gradient: linear-gradient(to bottom right, #9780E5, #DE33BB); 366 | --color-primary-gradient-webkit: -webkit-linear-gradient(to bottom right, #9780E5, #DE33BB); 367 | --gradient-featured-speaker-card-01: linear-gradient(to bottom, #d600aad6 0%, #b80ab3b8 85.4%); 368 | --gradient-featured-speaker-card-02: linear-gradient(to bottom, #b80ab3b8 0%, #9b13bd9b 100%); 369 | --gradient-featured-speaker-card-03: linear-gradient(to bottom, #9b13bd9b 0%, #7d1dc67d 100%); 370 | --gradient-featured-speaker-card-04: linear-gradient(to bottom, #7d1dc67d 0%, #512bd451 100%); 371 | --gradient-featured-speaker-card-05: linear-gradient(to bottom, #512bd451 0%, #7d1dc67d 100%); 372 | --gradient-featured-speaker-card-06: linear-gradient(to bottom, #7d1dc67d 0%, #9b13bd9b 100%); 373 | --featured-speaker-card-foreground: var(--color-midnight-blue-primary); 374 | --color-link-foreground: #479EF5; 375 | --color-link-foreground-hover: #62ABF5; 376 | --color-link-foreground-active: #2886DE; 377 | --color-cookie-banner-foreground: var(--color-foreground); 378 | --color-cookie-banner-background: var(--color-neutrals-additionals-grey-16); 379 | --color-cookie-border: var(--color-brand-purple-tint-30); 380 | --color-dotnet-btn-focus-outline: var(--color-neutrals-light-white); 381 | --color-dotnet-solid-btn-accent-foreground: var(--color-neutrals-dark-grey-8); 382 | --color-dotnet-solid-btn-accent-background: var(--color-brand-purple-tint-25); 383 | --color-dotnet-solid-btn-accent-border: var(--color-brand-purple-tint-25); 384 | --color-dotnet-solid-btn-accent-foreground-hover: var(--color-neutrals-dark-grey-8); 385 | --color-dotnet-solid-btn-accent-background-hover: var(--color-brand-purple-tint-30); 386 | --color-dotnet-solid-btn-accent-border-hover: var(--color-brand-purple-tint-30); 387 | --color-dotnet-solid-btn-accent-foreground-active: var(--color-neutrals-dark-grey-8); 388 | --color-dotnet-solid-btn-accent-background-active: var(--color-brand-purple-tint-35); 389 | --color-dotnet-solid-btn-accent-border-active: var(--color-brand-purple-tint-35); 390 | --color-dotnet-outline-btn-accent-foreground: var(--color-brand-purple-tint-25); 391 | --color-dotnet-outline-btn-accent-background: transparent; 392 | --color-dotnet-outline-btn-accent-border: var(--color-brand-purple-tint-25); 393 | --color-dotnet-outline-btn-accent-foreground-hover: var(--color-brand-purple-tint-25); 394 | --color-dotnet-outline-btn-accent-background-hover: var(--color-brand-purple-shade-35); 395 | --color-dotnet-outline-btn-accent-border-hover: var(--color-brand-purple-tint-25); 396 | --color-dotnet-outline-btn-accent-foreground-active: var(--color-brand-purple-tint-25); 397 | --color-dotnet-outline-btn-accent-background-active: var(--color-brand-purple-shade-30); 398 | --color-dotnet-outline-btn-accent-border-active: var(--color-brand-purple-tint-25); 399 | --color-dotnet-link-btn-accent-foreground: var(--color-brand-purple-tint-25); 400 | --color-dotnet-link-btn-accent-background: transparent; 401 | --color-dotnet-link-btn-accent-border: transparent; 402 | --color-dotnet-link-btn-accent-foreground-hover: var(--color-brand-purple-tint-30); 403 | --color-dotnet-link-btn-accent-background-hover: transparent; 404 | --color-dotnet-link-btn-accent-border-hover: transparent; 405 | --color-dotnet-link-btn-accent-foreground-active: var(--color-brand-purple-tint-35); 406 | --color-dotnet-link-btn-accent-background-active: transparent; 407 | --color-dotnet-link-btn-accent-border-active: transparent; 408 | --color-dotnet-link-btn-neutral-foreground: var(--color-neutrals-light-grey-96); 409 | --color-dotnet-link-btn-neutral-background: transparent; 410 | --color-dotnet-link-btn-neutral-border: transparent; 411 | --color-dotnet-link-btn-neutral-foreground-hover: var(--color-neutrals-light-grey-98); 412 | --color-dotnet-link-btn-neutral-background-hover: transparent; 413 | --color-dotnet-link-btn-neutral-border-hover: transparent; 414 | --color-dotnet-link-btn-neutral-foreground-active: var(--color-neutrals-light-white); 415 | --color-dotnet-link-btn-neutral-background-active: transparent; 416 | --color-dotnet-link-btn-neutral-border-active: transparent; 417 | --color-dotnet-menu-popover-focus-border: var(--color-neutrals-light-white); 418 | --color-dotnet-menu-popover-foreground: var(--color-neutrals-light-white); 419 | --color-dotnet-menu-popover-background: var(--color-neutrals-additionals-grey-16); 420 | --color-dotnet-menu-popover-foreground-hover: var(--color-neutrals-light-white); 421 | --color-dotnet-menu-popover-background-hover: var(--color-neutrals-dark-grey-24); 422 | --color-dotnet-menu-popover-foreground-active: var(--color-neutrals-light-white); 423 | --color-dotnet-menu-popover-background-active: var(--color-neutrals-additionals-grey-28); 424 | --color-dotnet-content-region-background: var(--color-neutrals-additionals-grey-16); 425 | --dotnet-speaker-card-foreground: var(--color-neutrals-dark-grey-68); 426 | --dotnet-speaker-card-background: var(--color-neutrals-additionals-grey-12); 427 | --dotnet-speaker-card-box-shadow: 0 2px 10px 4px rgba(65, 65, 65, 0.2); 428 | --dotnet-speaker-card-box-shadow-hover: 0 2px 10px 4px rgba(65, 65, 65, 0.8); 429 | --color-agenda-time: var(--color-neutrals-dark-grey-68); 430 | --color-agenda-summaries: var(--color-neutrals-dark-grey-70); 431 | --color-agenda-speaker: var(--color-brand-purple-tint-35); 432 | --color-local-events-date: var(--color-neutrals-dark-grey-84); 433 | --color-form-input-foreground: var(--color-foreground); 434 | --color-form-input-background: var(--color-neutrals-additionals-grey-16); 435 | --color-form-input-border: var(--color-neutrals-dark-grey-36); 436 | --logo-dark-display: block; 437 | --logo-light-display: none; 438 | } 439 | } 440 | 441 | body { 442 | -webkit-font-smoothing: antialiased; 443 | font-family: var(--base-font-family); 444 | color: var(--color-foreground); 445 | background-color: var(--color-background); 446 | -moz-osx-font-smoothing: grayscale; 447 | margin-top: 2rem; 448 | margin-left: 3rem; 449 | margin-right: 3rem; 450 | } 451 | 452 | textarea, 453 | input, 454 | button { 455 | font-size: 1rem; 456 | font-family: inherit; 457 | border: none; 458 | border-radius: 8px; 459 | padding: 0.5rem 0.75rem; 460 | box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.2), 0 1px 2px hsla(0, 0%, 0%, 0.2); 461 | background-color: white; 462 | line-height: 1.5; 463 | margin: 0; 464 | } 465 | 466 | button { 467 | color: var(--color-dotnet-solid-btn-accent-foreground); 468 | border-color: var(--color-dotnet-outline-btn-accent-border); 469 | background-color: var(--color-dotnet-solid-btn-accent-background); 470 | } 471 | 472 | textarea:hover, 473 | input:hover, 474 | button:hover { 475 | box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.6), 0 1px 2px hsla(0, 0%, 0%, 0.2); 476 | } 477 | 478 | button:active { 479 | box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.4); 480 | transform: translateY(1px); 481 | } 482 | 483 | #root { 484 | height: 100%; 485 | width: 100%; 486 | } 487 | 488 | #error>* { 489 | color: red; 490 | padding-left: 2rem; 491 | padding-right: 2rem; 492 | } 493 | 494 | #searchbox { 495 | padding-left: 2rem; 496 | padding-right: 2rem; 497 | } 498 | 499 | #searchbox>div form { 500 | position: relative; 501 | } 502 | 503 | #searchbox>div form input[type="search"] { 504 | width: 100%; 505 | padding-left: 2rem; 506 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='%23999' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' /%3E%3C/svg%3E"); 507 | background-repeat: no-repeat; 508 | background-position: 0.625rem 0.75rem; 509 | background-size: 1rem; 510 | position: relative; 511 | } 512 | 513 | #searchbox>div form input[type="search"].loading { 514 | background-image: none; 515 | } 516 | 517 | #searchbox>div form button { 518 | margin-left: 1rem; 519 | margin-top: 1rem; 520 | } 521 | 522 | #search-spinner { 523 | width: 1rem; 524 | height: 1rem; 525 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath stroke='%23000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M20 4v5h-.582m0 0a8.001 8.001 0 00-15.356 2m15.356-2H15M4 20v-5h.581m0 0a8.003 8.003 0 0015.357-2M4.581 15H9' /%3E%3C/svg%3E"); 526 | animation: spin 1s infinite linear; 527 | position: absolute; 528 | left: 0.625rem; 529 | top: 0.75rem; 530 | } 531 | 532 | @keyframes spin { 533 | from { 534 | transform: rotate(0deg); 535 | } 536 | 537 | to { 538 | transform: rotate(360deg); 539 | } 540 | } 541 | 542 | #sessions.loading { 543 | opacity: 0.25; 544 | transition: opacity 200ms; 545 | transition-delay: 200ms; 546 | } 547 | 548 | .font-title-2 { 549 | font-size: 1.75rem; 550 | font-family: var(--title-font-family); 551 | font-weight: 700; 552 | font-style: normal; 553 | line-height: 2.5rem; 554 | text-decoration: none; 555 | text-transform: none; 556 | color: var(--color-title-font); 557 | } 558 | 559 | .font-subtitle-2 { 560 | font-size: 1.125rem; 561 | font-family: var(--base-font-family); 562 | font-weight: 600; 563 | font-style: normal; 564 | line-height: 1.688rem; 565 | margin-top: -0.1rem; 566 | text-decoration: none; 567 | text-transform: none; 568 | color: var(--color-title-font); 569 | } 570 | 571 | h1 { 572 | background: var(--color-primary-gradient); 573 | background-clip: text; 574 | -webkit-background: var(--color-primary-gradient-webkit); 575 | -webkit-background-clip: text; 576 | -webkit-text-fill-color: transparent; 577 | } 578 | 579 | h1.font-title-2 { 580 | font-size:2.5rem 581 | } 582 | 583 | .session-similarity { 584 | color: var(--color-agenda-speaker); 585 | margin-bottom: 4px; 586 | margin-top: -1.75rem; 587 | } 588 | 589 | .font-stronger { 590 | font-weight: 700; 591 | } 592 | 593 | #root a { 594 | color: var(--color-link-foreground); 595 | text-decoration: underline; 596 | border: solid var(--dotnet-btn-border-width) transparent; 597 | border-radius: var(--dotnet-btn-corner-radius); 598 | } 599 | 600 | #root a:hover { 601 | color: var(--color-link-foreground-hover); 602 | text-decoration: underline; 603 | } 604 | 605 | #root a:active { 606 | color: var(--color-link-foreground-active); 607 | text-decoration: underline; 608 | } 609 | 610 | #root a:focus-visible { 611 | outline: solid var(--dotnet-btn-focus-outline-width) var(--color-dotnet-btn-focus-outline); 612 | outline-offset: var(--dotnet-btn-focus-outline-offset); 613 | } 614 | -------------------------------------------------------------------------------- /client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | import { 4 | createBrowserRouter, 5 | RouterProvider, 6 | useRouteLoaderData, 7 | } from "react-router-dom"; 8 | 9 | import "./index.css"; 10 | 11 | import Root, { loader as rootLoader } from "./routes/root"; 12 | import SessionsList, { loader as sessionsListLoader } from "./routes/sessionsList"; 13 | 14 | const router = createBrowserRouter([ 15 | { 16 | path: "/", 17 | element: , 18 | loader: rootLoader, 19 | children: [{ 20 | index: true, 21 | element: , 22 | loader: sessionsListLoader, 23 | }] 24 | }, 25 | ]); 26 | 27 | ReactDOM.createRoot(document.getElementById("root")).render( 28 | 29 | 30 | 31 | ); -------------------------------------------------------------------------------- /client/src/routes/root.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { useLoaderData, Outlet, Await, defer } from "react-router-dom"; 3 | import { getSessionsCount } from "../sessions"; 4 | import { getUserInfo } from "../user"; 5 | import ls from 'localstorage-slim'; 6 | 7 | export async function loader() { 8 | const sessionsCount = getSessionsCount(); 9 | const userInfo = await getUserInfo(); 10 | return defer({ userInfo, sessionsCount }); 11 | } 12 | 13 | function showSessionCount(sessionsCount) { 14 | var sc = sessionsCount; 15 | if (sc == undefined) { 16 | sc = ls.get("sessionsCount"); 17 | } else { 18 | ls.set("sessionsCount", sc, { ttl: 60 * 24 * 7 }); 19 | } 20 | if (sc == undefined) { 21 | return (

Loading session count...

); 22 | } 23 | return ( 24 |

There are {sc} sessions indexed so far.

25 | ); 26 | } 27 | 28 | 29 | export default function Root() { 30 | const { userInfo, sessionsCount } = useLoaderData(); 31 | 32 | return ( 33 | <> 34 | 56 |
57 | 58 |
59 | 60 | ); 61 | } -------------------------------------------------------------------------------- /client/src/routes/sessionsList.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Form, useLoaderData, useNavigation, } from "react-router-dom"; 3 | import { getSessions } from "../sessions"; 4 | 5 | export async function loader({ request }) { 6 | const url = new URL(request.url); 7 | const searchQuery = url.searchParams.get("q") ?? ""; 8 | var isSearch = (searchQuery == "") ? false : true; 9 | var { sessions, errorInfo } = isSearch ? await getSessions(searchQuery) : {sessions:[], errorInfo:null}; 10 | if (!Array.isArray(sessions)) { 11 | errorInfo = { errorMessage: "Error: sessions is not an array" }; 12 | sessions = []; 13 | } 14 | return { sessions, searchQuery, isSearch, errorInfo }; 15 | } 16 | 17 | export default function SessionsList() { 18 | const { sessions, searchQuery, isSearch, errorInfo } = useLoaderData(); 19 | const navigation = useNavigation(); 20 | 21 | const searching = 22 | navigation.location && 23 | new URLSearchParams(navigation.location.search).has( 24 | "q" 25 | ); 26 | 27 | useEffect(() => { 28 | document.getElementById("q").value = searchQuery; 29 | }, [searchQuery]); 30 | 31 | return ( 32 | <> 33 |