feat: moved guides (#3460)
### **PR Type** Enhancement ___ ### **Description** - Introduce React Apollo & React Query example projects - Configure GraphQL codegen and generate typed hooks - Implement `AuthProvider` for Nhost session management - Add pages (Home, SignIn, SignUp, Profile) and components ___ ### Diagram Walkthrough ```mermaid flowchart LR CG["Codegen Config"] -- generates --> GT["GraphQL Types & Hooks"] AP["AuthProvider"] -- provides --> APP["App (Router)"] GC["Apollo/Query Client"] -- used by --> APP APP -- routes --> Home["Home Page"] Home -- queries/mutations --> GT ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Dependencies</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>graphql.ts</strong><dd><code>Add generated GraphQL types and operations</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-f0ffc6fbc739aa455cf3967c263549e2acf4922dd63d9d2fd33ef9975d958b47">+6994/-0</a></td> </tr> <tr> <td><strong>graphql.ts</strong><dd><code>Add generated GraphQL types and operations</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-08b9f52995757f95a31a6d4d7bafb1f1a473ae338f9d11f343956048d13a0b58">+6944/-0</a></td> </tr> </table></details></td></tr><tr><td><strong>Configuration changes</strong></td><td><details><summary>4 files</summary><table> <tr> <td><strong>apolloClient.ts</strong><dd><code>Setup Apollo client with Nhost auth link</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-f0211d31ead8e27f200356bf615f34eee1e96ec6b068039a36b1506d32c35692">+53/-0</a> </td> </tr> <tr> <td><strong>queryHooks.ts</strong><dd><code>Create React Query authenticated fetcher</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-45c18c55c9756b413c5d4b9a71bd04a53d78706eb9a2a611f2fd0a1ab7ce6b51">+33/-0</a> </td> </tr> <tr> <td><strong>vite.config.ts</strong><dd><code>Add Vite config for React Apollo example</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-146d3d4bcf230a225998f3c68de6ffa9f19e16b85bb5ca882608d76e7b086566">+7/-0</a> </td> </tr> <tr> <td><strong>vite.config.ts</strong><dd><code>Add Vite config for React Query example</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-22a14bf686832a47057d73f552546a1d56a1f34cd34666acf741940feca0605f">+7/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>14 files</summary><table> <tr> <td><strong>AuthProvider.tsx</strong><dd><code>Implement Nhost AuthProvider for React Apollo</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-71a439d2114fc41855bdb2041134bbc4de64f886f06f3f50ed2fd31cc61c84a5">+175/-0</a> </td> </tr> <tr> <td><strong>AuthProvider.tsx</strong><dd><code>Implement Nhost AuthProvider for React Query</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-34697170eae721899cacb8faca334b60793b405b3f2ee327fecd4d6944338ed1">+175/-0</a> </td> </tr> <tr> <td><strong>Home.tsx</strong><dd><code>Add Home page with Apollo queries</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-407aac78333f6d47a86500b3e0c2311f1967ce50ba1a7ef4f022b8a50c013160">+203/-0</a> </td> </tr> <tr> <td><strong>Home.tsx</strong><dd><code>Add Home page with React Query hooks</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-c91862e4a735090baf145ed64f347a460446a25b8d5061b6b928b88ddb146008">+204/-0</a> </td> </tr> <tr> <td><strong>SignUp.tsx</strong><dd><code>Add SignUp page and form</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-9a841d153068ba2ba45e3b00c3979eca2048037670945ff079bce6645780f447">+127/-0</a> </td> </tr> <tr> <td><strong>SignUp.tsx</strong><dd><code>Add SignUp page and form</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-53b2663ba20acd8379c5dbbb671dd0c1badbb24955c7b9b7d9115843bd9ea859">+127/-0</a> </td> </tr> <tr> <td><strong>SignIn.tsx</strong><dd><code>Add SignIn page and form</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-fd0d7f5b422c5c3ec70fdeb95da10479b90132a571de56307d9b44fc69aaf27b">+120/-0</a> </td> </tr> <tr> <td><strong>SignIn.tsx</strong><dd><code>Add SignIn page and form</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-ec047013e51fbbbc17b641287e763558c5182325be0fe86d44e439eeca6e9ad1">+120/-0</a> </td> </tr> <tr> <td><strong>Profile.tsx</strong><dd><code>Add Profile page display</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-53e4cf70b4b84819e216a602241d5379b9402a2412af122b5a870ec0bc8cd6a1">+66/-0</a> </td> </tr> <tr> <td><strong>Profile.tsx</strong><dd><code>Add Profile page display</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-13fbb13999bb0001387c3d9a5015974ffece9575c1e6240a21157a07090b3374">+66/-0</a> </td> </tr> <tr> <td><strong>Navigation.tsx</strong><dd><code>Add navigation bar component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-eb031d8883524edeab44c5b665a2429cffb1bac75d8f0dc1dc7bc3c42aa9c068">+85/-0</a> </td> </tr> <tr> <td><strong>Navigation.tsx</strong><dd><code>Add navigation bar component</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-3d34860dbf6645315068dd62f3be52745294be2729e4dc718e40ad0125669204">+85/-0</a> </td> </tr> <tr> <td><strong>ProtectedRoute.tsx</strong><dd><code>Add protected route wrapper</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-55c80a4fab944fa1c3210cce0fd0092776a01046ee9893c88c69d0c5f5f5458b">+26/-0</a> </td> </tr> <tr> <td><strong>ProtectedRoute.tsx</strong><dd><code>Add protected route wrapper</code> </dd></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-bfc4913eb77b8cbe8115f7833bfdf1aad595496f3160dbf43eabf829f0d94f5a">+26/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>39 files</summary><table> <tr> <td><strong>examples_guides_checks.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-56c96ece925a38bd3e84d3fff001760c68d40744013d2b3458ad445a686a0c36">+94/-0</a> </td> </tr> <tr> <td><strong>project.nix</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-f6086e59795d16f4c73cb0e5f90b986e288deb5f176c80c5c035f1703ff86760">+1/-3</a> </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-85a3083c78e211e9eb36d741342bcbc85a1a0c375060f45c5426b560196de27f">+17/-0</a> </td> </tr> <tr> <td><strong>package.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-4095856b4a3f808fcab300063ba57b9e84d2cd2b80e244b69d48d13ddadf161d">+13/-0</a> </td> </tr> <tr> <td><strong>project.nix</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-65c8830b4c3074e59267275284f270f4dfa95ce2722d7634e3e3c77f66f8235e">+105/-0</a> </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-b79af2df973af3966472409744a06f7e930008c43216cee8ea91af504bf569fe">+391/-0</a> </td> </tr> <tr> <td><strong>biome.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-bee8a9ce71bf1a5641c7bc5aa57f0ca5b4ab5e6a1fc0c08067cb02caa48048a9">+7/-0</a> </td> </tr> <tr> <td><strong>codegen-wrapper.sh</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-3d8141868656cb3c65352257558703f27b1f73b417f8a3223a595d31ffda1b4a">+27/-0</a> </td> </tr> <tr> <td><strong>codegen.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-88f3c353c9bfd474e259a4e6a70b4425032b1d6b3c228046c36333b22bdb027e">+44/-0</a> </td> </tr> <tr> <td><strong>index.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-e26ca0f00bc4456ef2ec04d470a864c72252bff2a9d34aa068bd6fcee9bd837c">+13/-0</a> </td> </tr> <tr> <td><strong>package.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-219f91ff32e8ae8ff4fb20d06f91c0d4501d50dcaf34a4012aa0829c61eca41b">+36/-0</a> </td> </tr> <tr> <td><strong>schema.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-1aef57e8c82d9f02e7c40f665ffbc384489beff7b299a8d30f1b2a00ba54676b">+10143/-0</a></td> </tr> <tr> <td><strong>App.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-3939716d6e04d989c77f1b141c7893b8cad49325d77146df613ea763da566029">+56/-0</a> </td> </tr> <tr> <td><strong>index.css</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-42378718fb21f3858c4d3ced50bff70946a0a63d6f5afabf5d29e01b9d7e58cb">+552/-0</a> </td> </tr> <tr> <td><strong>queries.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-a72744935ef245ce03e31a475ece30582d3fa9d8295a1338d2fdf3d64f52875a">+28/-0</a> </td> </tr> <tr> <td><strong>main.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-fe10a290150d971b54595c18a0e3b9486ddd237611225ec4fae333c893403a96">+33/-0</a> </td> </tr> <tr> <td><strong>Home.css</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-34b39976cc1741190f571fe587e77290bfec3f7248b8dab7cfe2ef3832f39ad0">+217/-0</a> </td> </tr> <tr> <td><strong>vite-env.d.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-76ec290ea25ad1dd3d6a40aceb23964e6af759e9d223d0227ea7a9b4c27a4c1a">+11/-0</a> </td> </tr> <tr> <td><strong>tsconfig.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-1c4ff207f3f497bf6eb395d765c67c43197a8141faf8e0b78975749d7ef8de7b">+6/-0</a> </td> </tr> <tr> <td><strong>tsconfig.node.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-2c47cac1784e69cc8ee9de2b8654939b7699e627e6996135af736d086471cbda">+4/-0</a> </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-f8cffe1a6c5ab9062008bdc81cbdbc19ff73c414e852e6cc630288c935cb85c5">+493/-0</a> </td> </tr> <tr> <td><strong>biome.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-f3c8d8d56e583f7496332f58d85b8f2fc179364af6d7b6db2050cf8f3019449d">+7/-0</a> </td> </tr> <tr> <td><strong>codegen-wrapper.sh</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-697fec800dc8ea79d58ac3650f21d252340f04c760df18c060c24523a7324967">+31/-0</a> </td> </tr> <tr> <td><strong>codegen.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-9b2b4b1be5005479ea56b7dbb26f8f34e9b39ab005c4df5e5547caef4bb8176e">+52/-0</a> </td> </tr> <tr> <td><strong>index.html</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-6a4d60e607c923818133e58b28b06f9b749a492a500a6b2899e1f2fd991ad03b">+13/-0</a> </td> </tr> <tr> <td><strong>package.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-e80d8d01acf5048bc7c1767cf248b07e67df6a13914ac02d43c8b180b0178ddc">+35/-0</a> </td> </tr> <tr> <td><strong>schema.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-938b6a319b1d5f5234f3e80b49c3d9c429a45569fb777ef9310bdbdeb3f485e2">+10143/-0</a></td> </tr> <tr> <td><strong>App.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-9a06f558adc00bb736ae330d10f276dd360d3b2e08814d424392470d6b19af3b">+56/-0</a> </td> </tr> <tr> <td><strong>index.css</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-8fe80a812b88da1a00debcd607d5a7300a01558103403d6beba1c8a514d1cae0">+552/-0</a> </td> </tr> <tr> <td><strong>QueryProvider.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-5e3488d3b7deca24d0d26f4af88d201e7678662e6531f4e4cb511f99d32854d5">+27/-0</a> </td> </tr> <tr> <td><strong>queries.graphql</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-6506f25586ed4d14db727ca7ee5be4e299c29dbbfcdbcbc4f81e49a4ae9b5fb5">+28/-0</a> </td> </tr> <tr> <td><strong>main.tsx</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-2d90aa9771205fab3d46a5d28686eae47e929a9ec6070dc3d564d37d6324768f">+22/-0</a> </td> </tr> <tr> <td><strong>Home.css</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-ef3665d0c09e727cc06d1d429d983c3767f8b65add4bdbe5b095d3816ebbce1d">+217/-0</a> </td> </tr> <tr> <td><strong>vite-env.d.ts</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-c931c9d2529208b5808e22f0a18c44b7cff5e88d1592b8f472f46bcdeda844b5">+11/-0</a> </td> </tr> <tr> <td><strong>tsconfig.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-097ce507ecd096e978eb5013155ceb912fe10b0b6de8ee4a0f13ff08565e1cae">+6/-0</a> </td> </tr> <tr> <td><strong>tsconfig.node.json</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-12b373ca0bdd9a2bc826e660ccdccc9573bd0ef54d592f2ebce0473e9568dde8">+4/-0</a> </td> </tr> <tr> <td><strong>flake.nix</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-206b9ce276ab5971a2489d75eb1b12999d4bf3843b7988cbe8d687cfde61dea0">+7/-40</a> </td> </tr> <tr> <td><strong>js.nix</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-bdae1ae0f0031ce2c2e0356df4460e40363922c9658b25c773ebfe29fe052cf9">+4/-1</a> </td> </tr> <tr> <td><strong>pnpm-workspace.yaml</strong></td> <td><a href="https://github.com/nhost/nhost/pull/3460/files#diff-18ae0a0fab29a7db7aded913fd05f30a2c8f6c104fadae86c9d217091709794c">+1/-0</a> </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
This commit is contained in:
94
.github/workflows/examples_guides_checks.yaml
vendored
Normal file
94
.github/workflows/examples_guides_checks.yaml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: "examples/guides: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_guides_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# codegen
|
||||
- 'tools/codegen/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# nhpst-js
|
||||
- 'packages/nhost-js/**'
|
||||
|
||||
# guides
|
||||
- 'examples/guides/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
5
.npmrc
5
.npmrc
@@ -1,3 +1,8 @@
|
||||
prefer-workspace-packages = true
|
||||
auto-install-peers = true
|
||||
|
||||
# without this setting, pnpm breaks monorepos with multiple versions of the same package
|
||||
shared-workspace-lockfile = false
|
||||
|
||||
# do not enable back, this leads to unlisted dependencies being used
|
||||
hoist = false
|
||||
|
||||
@@ -223,5 +223,21 @@
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
},
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
"@uiw/codemirror-theme-bbedit": {
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@uiw/codemirror-theme-github": {
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
425
dashboard/pnpm-lock.yaml
generated
425
dashboard/pnpm-lock.yaml
generated
@@ -4,6 +4,8 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
packageExtensionsChecksum: sha256-gRFeykwiwMfEE6etcYx6N48XwVeKzxbqNveL7KTQgSQ=
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -160,7 +162,7 @@ importers:
|
||||
version: 4.22.1(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)
|
||||
'@uiw/react-codemirror':
|
||||
specifier: ^4.21.25
|
||||
version: 4.22.1(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.4)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 4.22.1(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.4)(codemirror@5.65.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
bcryptjs:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
@@ -356,7 +358,7 @@ importers:
|
||||
version: 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/addon-essentials':
|
||||
specifier: ^6.5.16
|
||||
version: 6.5.16(@babel/core@7.26.10)(@storybook/builder-webpack5@6.5.16(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))
|
||||
version: 6.5.16(@babel/core@7.26.10)(@storybook/builder-webpack5@6.5.16(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))
|
||||
'@storybook/addon-interactions':
|
||||
specifier: ^6.5.16
|
||||
version: 6.5.16(@types/react@18.2.73)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
@@ -365,13 +367,13 @@ importers:
|
||||
version: 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/addon-postcss':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
version: 2.0.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
'@storybook/builder-webpack5':
|
||||
specifier: ^6.5.16
|
||||
version: 6.5.16(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
version: 6.5.16(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
'@storybook/manager-webpack5':
|
||||
specifier: ^6.5.16
|
||||
version: 6.5.16(encoding@0.1.13)(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
version: 6.5.16(encoding@0.1.13)(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
'@storybook/react':
|
||||
specifier: ^7.6.17
|
||||
version: 7.6.20(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
@@ -449,7 +451,7 @@ importers:
|
||||
version: 10.4.20(postcss@8.5.3)
|
||||
babel-loader:
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.25.9))
|
||||
version: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.18.20))
|
||||
babel-plugin-transform-remove-console:
|
||||
specifier: ^6.9.4
|
||||
version: 6.9.4
|
||||
@@ -1630,159 +1632,135 @@ packages:
|
||||
'@emotion/weak-memoize@0.3.1':
|
||||
resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@@ -5264,9 +5242,6 @@ packages:
|
||||
codemirror@5.65.16:
|
||||
resolution: {integrity: sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==}
|
||||
|
||||
codemirror@6.0.1:
|
||||
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
|
||||
|
||||
collapse-white-space@1.0.6:
|
||||
resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==}
|
||||
|
||||
@@ -5876,11 +5851,11 @@ packages:
|
||||
esbuild-register@3.5.0:
|
||||
resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.25.0'
|
||||
esbuild: '>=0.12 <1'
|
||||
|
||||
esbuild@0.25.9:
|
||||
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
|
||||
engines: {node: '>=18'}
|
||||
esbuild@0.18.20:
|
||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.2.0:
|
||||
@@ -6333,7 +6308,7 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>= 6'
|
||||
typescript: '>= 2.7'
|
||||
vue-template-compiler: '>=3.0.0'
|
||||
vue-template-compiler: '*'
|
||||
webpack: '>= 4'
|
||||
peerDependenciesMeta:
|
||||
eslint:
|
||||
@@ -12150,82 +12125,70 @@ snapshots:
|
||||
|
||||
'@emotion/weak-memoize@0.3.1': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
'@esbuild/android-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
'@esbuild/android-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
'@esbuild/android-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
'@esbuild/darwin-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
'@esbuild/darwin-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
'@esbuild/freebsd-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
'@esbuild/freebsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
'@esbuild/linux-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
'@esbuild/linux-arm@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
'@esbuild/linux-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
'@esbuild/linux-loong64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
'@esbuild/linux-mips64el@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
'@esbuild/linux-ppc64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
'@esbuild/linux-riscv64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
'@esbuild/linux-s390x@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
'@esbuild/linux-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
'@esbuild/netbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
'@esbuild/openbsd-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
'@esbuild/sunos-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
'@esbuild/win32-arm64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
'@esbuild/win32-ia32@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
'@esbuild/win32-x64@0.18.20':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.8.0(eslint@8.57.0)':
|
||||
@@ -14278,7 +14241,7 @@ snapshots:
|
||||
- webpack-cli
|
||||
- webpack-command
|
||||
|
||||
'@storybook/addon-docs@6.5.16(@babel/core@7.26.10)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))':
|
||||
'@storybook/addon-docs@6.5.16(@babel/core@7.26.10)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))':
|
||||
dependencies:
|
||||
'@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.10)
|
||||
'@babel/preset-env': 7.24.7(@babel/core@7.26.10)
|
||||
@@ -14298,7 +14261,7 @@ snapshots:
|
||||
'@storybook/source-loader': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/theming': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
babel-loader: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.25.9))
|
||||
babel-loader: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.18.20))
|
||||
core-js: 3.37.1
|
||||
fast-deep-equal: 3.1.3
|
||||
global: 4.4.0
|
||||
@@ -14321,13 +14284,13 @@ snapshots:
|
||||
- webpack-cli
|
||||
- webpack-command
|
||||
|
||||
'@storybook/addon-essentials@6.5.16(@babel/core@7.26.10)(@storybook/builder-webpack5@6.5.16(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))':
|
||||
'@storybook/addon-essentials@6.5.16(@babel/core@7.26.10)(@storybook/builder-webpack5@6.5.16(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
'@storybook/addon-actions': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/addon-backgrounds': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/addon-controls': 6.5.16(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
'@storybook/addon-docs': 6.5.16(@babel/core@7.26.10)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))
|
||||
'@storybook/addon-docs': 6.5.16(@babel/core@7.26.10)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))
|
||||
'@storybook/addon-measure': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/addon-outline': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/addon-toolbars': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -14340,10 +14303,10 @@ snapshots:
|
||||
regenerator-runtime: 0.13.11
|
||||
ts-dedent: 2.2.0
|
||||
optionalDependencies:
|
||||
'@storybook/builder-webpack5': 6.5.16(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
'@storybook/builder-webpack5': 6.5.16(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
transitivePeerDependencies:
|
||||
- '@storybook/mdx2-csf'
|
||||
- eslint
|
||||
@@ -14430,13 +14393,13 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@storybook/addon-postcss@2.0.0(webpack@5.97.1(esbuild@0.25.9))':
|
||||
'@storybook/addon-postcss@2.0.0(webpack@5.97.1(esbuild@0.18.20))':
|
||||
dependencies:
|
||||
'@storybook/node-logger': 6.5.16
|
||||
css-loader: 3.6.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
css-loader: 3.6.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
postcss: 7.0.39
|
||||
postcss-loader: 4.3.0(postcss@7.0.39)(webpack@5.97.1(esbuild@0.25.9))
|
||||
style-loader: 1.3.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
postcss-loader: 4.3.0(postcss@7.0.39)(webpack@5.97.1(esbuild@0.18.20))
|
||||
style-loader: 1.3.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
transitivePeerDependencies:
|
||||
- webpack
|
||||
|
||||
@@ -14508,7 +14471,7 @@ snapshots:
|
||||
ts-dedent: 2.2.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
'@storybook/builder-webpack5@6.5.16(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)':
|
||||
'@storybook/builder-webpack5@6.5.16(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
'@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -14527,27 +14490,27 @@ snapshots:
|
||||
'@storybook/store': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/theming': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@types/node': 16.18.105
|
||||
babel-loader: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.25.9))
|
||||
babel-loader: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.18.20))
|
||||
babel-plugin-named-exports-order: 0.0.2
|
||||
browser-assert: 1.2.1
|
||||
case-sensitive-paths-webpack-plugin: 2.4.0
|
||||
core-js: 3.37.1
|
||||
css-loader: 5.2.7(webpack@5.97.1(esbuild@0.25.9))
|
||||
fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))
|
||||
css-loader: 5.2.7(webpack@5.97.1(esbuild@0.18.20))
|
||||
fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))
|
||||
glob: 7.2.3
|
||||
glob-promise: 3.4.0(glob@7.2.3)
|
||||
html-webpack-plugin: 5.6.3(webpack@5.97.1(esbuild@0.25.9))
|
||||
html-webpack-plugin: 5.6.3(webpack@5.97.1(esbuild@0.18.20))
|
||||
path-browserify: 1.0.1
|
||||
process: 0.11.10
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
stable: 0.1.8
|
||||
style-loader: 2.0.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
terser-webpack-plugin: 5.3.11(esbuild@0.25.9)(webpack@5.97.1(esbuild@0.25.9))
|
||||
style-loader: 2.0.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
terser-webpack-plugin: 5.3.11(esbuild@0.18.20)(webpack@5.97.1(esbuild@0.18.20))
|
||||
ts-dedent: 2.2.0
|
||||
util-deprecate: 1.0.2
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack-dev-middleware: 4.3.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
webpack-dev-middleware: 4.3.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
webpack-hot-middleware: 2.26.1
|
||||
webpack-virtual-modules: 0.4.6
|
||||
optionalDependencies:
|
||||
@@ -14643,7 +14606,7 @@ snapshots:
|
||||
regenerator-runtime: 0.13.11
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
'@storybook/core-client@6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))':
|
||||
'@storybook/core-client@6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))':
|
||||
dependencies:
|
||||
'@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/channel-postmessage': 6.5.16
|
||||
@@ -14667,7 +14630,7 @@ snapshots:
|
||||
ts-dedent: 2.2.0
|
||||
unfetch: 4.2.0
|
||||
util-deprecate: 1.0.2
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
@@ -14749,8 +14712,8 @@ snapshots:
|
||||
'@types/node-fetch': 2.6.11
|
||||
'@types/pretty-hrtime': 1.0.3
|
||||
chalk: 4.1.2
|
||||
esbuild: 0.25.9
|
||||
esbuild-register: 3.5.0(esbuild@0.25.9)
|
||||
esbuild: 0.18.20
|
||||
esbuild-register: 3.5.0(esbuild@0.18.20)
|
||||
file-system-cache: 2.3.0
|
||||
find-cache-dir: 3.3.2
|
||||
find-up: 5.0.0
|
||||
@@ -14824,27 +14787,27 @@ snapshots:
|
||||
- react
|
||||
- react-dom
|
||||
|
||||
'@storybook/manager-webpack5@6.5.16(encoding@0.1.13)(esbuild@0.25.9)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)':
|
||||
'@storybook/manager-webpack5@6.5.16(encoding@0.1.13)(esbuild@0.18.20)(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
'@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.26.10)
|
||||
'@babel/preset-react': 7.24.6(@babel/core@7.26.10)
|
||||
'@storybook/addons': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/core-client': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9))
|
||||
'@storybook/core-client': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20))
|
||||
'@storybook/core-common': 6.5.16(eslint@8.57.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
'@storybook/node-logger': 6.5.16
|
||||
'@storybook/theming': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@storybook/ui': 6.5.16(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@types/node': 16.18.105
|
||||
babel-loader: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.25.9))
|
||||
babel-loader: 8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.18.20))
|
||||
case-sensitive-paths-webpack-plugin: 2.4.0
|
||||
chalk: 4.1.2
|
||||
core-js: 3.37.1
|
||||
css-loader: 5.2.7(webpack@5.97.1(esbuild@0.25.9))
|
||||
css-loader: 5.2.7(webpack@5.97.1(esbuild@0.18.20))
|
||||
express: 4.21.2
|
||||
find-up: 5.0.0
|
||||
fs-extra: 9.1.0
|
||||
html-webpack-plugin: 5.6.3(webpack@5.97.1(esbuild@0.25.9))
|
||||
html-webpack-plugin: 5.6.3(webpack@5.97.1(esbuild@0.18.20))
|
||||
node-fetch: 2.6.13(encoding@0.1.13)
|
||||
process: 0.11.10
|
||||
react: 18.2.0
|
||||
@@ -14852,13 +14815,13 @@ snapshots:
|
||||
read-pkg-up: 7.0.1
|
||||
regenerator-runtime: 0.13.11
|
||||
resolve-from: 5.0.0
|
||||
style-loader: 2.0.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
style-loader: 2.0.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
telejson: 6.0.8
|
||||
terser-webpack-plugin: 5.3.11(esbuild@0.25.9)(webpack@5.97.1(esbuild@0.25.9))
|
||||
terser-webpack-plugin: 5.3.11(esbuild@0.18.20)(webpack@5.97.1(esbuild@0.18.20))
|
||||
ts-dedent: 2.2.0
|
||||
util-deprecate: 1.0.2
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack-dev-middleware: 4.3.0(webpack@5.97.1(esbuild@0.25.9))
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
webpack-dev-middleware: 4.3.0(webpack@5.97.1(esbuild@0.18.20))
|
||||
webpack-virtual-modules: 0.4.6
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
@@ -15713,6 +15676,8 @@ snapshots:
|
||||
|
||||
'@uiw/codemirror-theme-bbedit@4.22.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@uiw/codemirror-themes': 4.22.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/language'
|
||||
@@ -15721,6 +15686,8 @@ snapshots:
|
||||
|
||||
'@uiw/codemirror-theme-github@4.22.1(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@uiw/codemirror-themes': 4.22.1(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/language'
|
||||
@@ -15739,7 +15706,7 @@ snapshots:
|
||||
'@codemirror/state': 6.4.1
|
||||
'@codemirror/view': 6.26.4
|
||||
|
||||
'@uiw/react-codemirror@4.22.1(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.4)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
'@uiw/react-codemirror@4.22.1(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.26.4)(codemirror@5.65.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@codemirror/commands': 6.5.0
|
||||
@@ -15747,7 +15714,7 @@ snapshots:
|
||||
'@codemirror/theme-one-dark': 6.1.2
|
||||
'@codemirror/view': 6.26.4
|
||||
'@uiw/codemirror-extensions-basic-setup': 4.22.1(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)(@lezer/common@1.2.1))(@codemirror/commands@6.5.0)(@codemirror/language@6.10.2)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)
|
||||
codemirror: 6.0.1(@lezer/common@1.2.1)
|
||||
codemirror: 5.65.16
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
@@ -16404,14 +16371,14 @@ snapshots:
|
||||
schema-utils: 2.7.1
|
||||
webpack: 4.47.0
|
||||
|
||||
babel-loader@8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.25.9)):
|
||||
babel-loader@8.3.0(@babel/core@7.26.10)(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
find-cache-dir: 3.3.2
|
||||
loader-utils: 2.0.4
|
||||
make-dir: 3.1.0
|
||||
schema-utils: 2.7.1
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
babel-plugin-apply-mdx-type-prop@1.6.22(@babel/core@7.12.9):
|
||||
dependencies:
|
||||
@@ -17043,18 +17010,6 @@ snapshots:
|
||||
|
||||
codemirror@5.65.16: {}
|
||||
|
||||
codemirror@6.0.1(@lezer/common@1.2.1):
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.4)(@lezer/common@1.2.1)
|
||||
'@codemirror/commands': 6.5.0
|
||||
'@codemirror/language': 6.10.2
|
||||
'@codemirror/lint': 6.8.0
|
||||
'@codemirror/search': 6.5.6
|
||||
'@codemirror/state': 6.4.1
|
||||
'@codemirror/view': 6.26.4
|
||||
transitivePeerDependencies:
|
||||
- '@lezer/common'
|
||||
|
||||
collapse-white-space@1.0.6: {}
|
||||
|
||||
collection-visit@1.0.0:
|
||||
@@ -17265,7 +17220,7 @@ snapshots:
|
||||
randombytes: 2.1.0
|
||||
randomfill: 1.0.4
|
||||
|
||||
css-loader@3.6.0(webpack@5.97.1(esbuild@0.25.9)):
|
||||
css-loader@3.6.0(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
cssesc: 3.0.0
|
||||
@@ -17280,9 +17235,9 @@ snapshots:
|
||||
postcss-value-parser: 4.2.0
|
||||
schema-utils: 2.7.1
|
||||
semver: 6.3.1
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
css-loader@5.2.7(webpack@5.97.1(esbuild@0.25.9)):
|
||||
css-loader@5.2.7(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
icss-utils: 5.1.0(postcss@8.5.3)
|
||||
loader-utils: 2.0.4
|
||||
@@ -17294,7 +17249,7 @@ snapshots:
|
||||
postcss-value-parser: 4.2.0
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.6.3
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
css-select@4.3.0:
|
||||
dependencies:
|
||||
@@ -17744,41 +17699,37 @@ snapshots:
|
||||
|
||||
es6-shim@0.35.8: {}
|
||||
|
||||
esbuild-register@3.5.0(esbuild@0.25.9):
|
||||
esbuild-register@3.5.0(esbuild@0.18.20):
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
esbuild: 0.25.9
|
||||
esbuild: 0.18.20
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
esbuild@0.25.9:
|
||||
esbuild@0.18.20:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.9
|
||||
'@esbuild/android-arm': 0.25.9
|
||||
'@esbuild/android-arm64': 0.25.9
|
||||
'@esbuild/android-x64': 0.25.9
|
||||
'@esbuild/darwin-arm64': 0.25.9
|
||||
'@esbuild/darwin-x64': 0.25.9
|
||||
'@esbuild/freebsd-arm64': 0.25.9
|
||||
'@esbuild/freebsd-x64': 0.25.9
|
||||
'@esbuild/linux-arm': 0.25.9
|
||||
'@esbuild/linux-arm64': 0.25.9
|
||||
'@esbuild/linux-ia32': 0.25.9
|
||||
'@esbuild/linux-loong64': 0.25.9
|
||||
'@esbuild/linux-mips64el': 0.25.9
|
||||
'@esbuild/linux-ppc64': 0.25.9
|
||||
'@esbuild/linux-riscv64': 0.25.9
|
||||
'@esbuild/linux-s390x': 0.25.9
|
||||
'@esbuild/linux-x64': 0.25.9
|
||||
'@esbuild/netbsd-arm64': 0.25.9
|
||||
'@esbuild/netbsd-x64': 0.25.9
|
||||
'@esbuild/openbsd-arm64': 0.25.9
|
||||
'@esbuild/openbsd-x64': 0.25.9
|
||||
'@esbuild/openharmony-arm64': 0.25.9
|
||||
'@esbuild/sunos-x64': 0.25.9
|
||||
'@esbuild/win32-arm64': 0.25.9
|
||||
'@esbuild/win32-ia32': 0.25.9
|
||||
'@esbuild/win32-x64': 0.25.9
|
||||
'@esbuild/android-arm': 0.18.20
|
||||
'@esbuild/android-arm64': 0.18.20
|
||||
'@esbuild/android-x64': 0.18.20
|
||||
'@esbuild/darwin-arm64': 0.18.20
|
||||
'@esbuild/darwin-x64': 0.18.20
|
||||
'@esbuild/freebsd-arm64': 0.18.20
|
||||
'@esbuild/freebsd-x64': 0.18.20
|
||||
'@esbuild/linux-arm': 0.18.20
|
||||
'@esbuild/linux-arm64': 0.18.20
|
||||
'@esbuild/linux-ia32': 0.18.20
|
||||
'@esbuild/linux-loong64': 0.18.20
|
||||
'@esbuild/linux-mips64el': 0.18.20
|
||||
'@esbuild/linux-ppc64': 0.18.20
|
||||
'@esbuild/linux-riscv64': 0.18.20
|
||||
'@esbuild/linux-s390x': 0.18.20
|
||||
'@esbuild/linux-x64': 0.18.20
|
||||
'@esbuild/netbsd-x64': 0.18.20
|
||||
'@esbuild/openbsd-x64': 0.18.20
|
||||
'@esbuild/sunos-x64': 0.18.20
|
||||
'@esbuild/win32-arm64': 0.18.20
|
||||
'@esbuild/win32-ia32': 0.18.20
|
||||
'@esbuild/win32-x64': 0.18.20
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
@@ -18522,7 +18473,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
eslint: 8.57.0
|
||||
|
||||
fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.25.9)):
|
||||
fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.0)(typescript@5.8.3)(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@types/json-schema': 7.0.15
|
||||
@@ -18538,7 +18489,7 @@ snapshots:
|
||||
semver: 7.6.3
|
||||
tapable: 1.1.3
|
||||
typescript: 5.8.3
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
optionalDependencies:
|
||||
eslint: 8.57.0
|
||||
|
||||
@@ -19082,7 +19033,7 @@ snapshots:
|
||||
|
||||
html-void-elements@1.0.5: {}
|
||||
|
||||
html-webpack-plugin@5.6.3(webpack@5.97.1(esbuild@0.25.9)):
|
||||
html-webpack-plugin@5.6.3(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
'@types/html-minifier-terser': 6.1.0
|
||||
html-minifier-terser: 6.1.0
|
||||
@@ -19090,7 +19041,7 @@ snapshots:
|
||||
pretty-error: 4.0.0
|
||||
tapable: 2.2.1
|
||||
optionalDependencies:
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
htmlparser2@6.1.0:
|
||||
dependencies:
|
||||
@@ -21125,7 +21076,7 @@ snapshots:
|
||||
postcss: 8.5.3
|
||||
ts-node: 10.9.2(@types/node@16.18.105)(typescript@5.8.3)
|
||||
|
||||
postcss-loader@4.3.0(postcss@7.0.39)(webpack@5.97.1(esbuild@0.25.9)):
|
||||
postcss-loader@4.3.0(postcss@7.0.39)(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
cosmiconfig: 7.1.0
|
||||
klona: 2.0.6
|
||||
@@ -21133,7 +21084,7 @@ snapshots:
|
||||
postcss: 7.0.39
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.6.3
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
postcss-modules-extract-imports@2.0.0:
|
||||
dependencies:
|
||||
@@ -22420,17 +22371,17 @@ snapshots:
|
||||
'@types/node': 16.18.105
|
||||
qs: 6.13.1
|
||||
|
||||
style-loader@1.3.0(webpack@5.97.1(esbuild@0.25.9)):
|
||||
style-loader@1.3.0(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
loader-utils: 2.0.4
|
||||
schema-utils: 2.7.1
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
style-loader@2.0.0(webpack@5.97.1(esbuild@0.25.9)):
|
||||
style-loader@2.0.0(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
loader-utils: 2.0.4
|
||||
schema-utils: 3.3.0
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
style-mod@4.1.2: {}
|
||||
|
||||
@@ -22563,16 +22514,16 @@ snapshots:
|
||||
webpack-sources: 1.4.3
|
||||
worker-farm: 1.7.0
|
||||
|
||||
terser-webpack-plugin@5.3.11(esbuild@0.25.9)(webpack@5.97.1(esbuild@0.25.9)):
|
||||
terser-webpack-plugin@5.3.11(esbuild@0.18.20)(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.30
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.0
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.37.0
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
optionalDependencies:
|
||||
esbuild: 0.25.9
|
||||
esbuild: 0.18.20
|
||||
|
||||
terser@4.8.1:
|
||||
dependencies:
|
||||
@@ -23178,7 +23129,7 @@ snapshots:
|
||||
|
||||
vite@4.5.14(@types/node@16.18.105)(terser@5.37.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.9
|
||||
esbuild: 0.18.20
|
||||
postcss: 8.5.3
|
||||
rollup: 3.29.5
|
||||
optionalDependencies:
|
||||
@@ -23314,7 +23265,7 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
webpack-dev-middleware@4.3.0(webpack@5.97.1(esbuild@0.25.9)):
|
||||
webpack-dev-middleware@4.3.0(webpack@5.97.1(esbuild@0.18.20)):
|
||||
dependencies:
|
||||
colorette: 1.4.0
|
||||
mem: 8.1.1
|
||||
@@ -23322,7 +23273,7 @@ snapshots:
|
||||
mime-types: 2.1.35
|
||||
range-parser: 1.2.1
|
||||
schema-utils: 3.3.0
|
||||
webpack: 5.97.1(esbuild@0.25.9)
|
||||
webpack: 5.97.1(esbuild@0.18.20)
|
||||
|
||||
webpack-hot-middleware@2.26.1:
|
||||
dependencies:
|
||||
@@ -23367,7 +23318,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
webpack@5.97.1(esbuild@0.25.9):
|
||||
webpack@5.97.1(esbuild@0.18.20):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.6
|
||||
@@ -23389,7 +23340,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.3.0
|
||||
tapable: 2.2.1
|
||||
terser-webpack-plugin: 5.3.11(esbuild@0.25.9)(webpack@5.97.1(esbuild@0.25.9))
|
||||
terser-webpack-plugin: 5.3.11(esbuild@0.18.20)(webpack@5.97.1(esbuild@0.18.20))
|
||||
watchpack: 2.4.2
|
||||
webpack-sources: 3.2.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -13,5 +13,20 @@
|
||||
"prettier": "^3.5.3",
|
||||
"typedoc": "^0.28.4",
|
||||
"typedoc-plugin-markdown": "^4.6.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
"@mintlify/link-rot@*": {
|
||||
"dependencies": {
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"@mintlify/scraping@*": {
|
||||
"dependencies": {
|
||||
"openapi-types": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
docs/pnpm-lock.yaml
generated
11
docs/pnpm-lock.yaml
generated
@@ -4,6 +4,8 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
packageExtensionsChecksum: sha256-4+NJJHoeDEOtWI2UxgTNLimXyrOojBs00S85/9Babm0=
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -3529,7 +3531,7 @@ snapshots:
|
||||
'@mintlify/cli@4.0.705(@types/node@24.3.0)(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@mintlify/common': 1.0.516(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@mintlify/link-rot': 3.0.652(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)
|
||||
'@mintlify/link-rot': 3.0.652(@types/react@19.1.12)(typescript@5.9.2)
|
||||
'@mintlify/models': 0.0.225
|
||||
'@mintlify/prebuild': 1.0.640(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)
|
||||
'@mintlify/previewing': 4.0.688(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(typescript@5.9.2)
|
||||
@@ -3610,13 +3612,15 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@mintlify/link-rot@3.0.652(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)':
|
||||
'@mintlify/link-rot@3.0.652(@types/react@19.1.12)(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@mintlify/common': 1.0.516(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@mintlify/prebuild': 1.0.640(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)
|
||||
'@mintlify/previewing': 4.0.688(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(typescript@5.9.2)
|
||||
'@mintlify/validation': 0.1.458
|
||||
fs-extra: 11.3.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
unist-util-visit: 4.1.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
@@ -3624,9 +3628,7 @@ snapshots:
|
||||
- bufferutil
|
||||
- debug
|
||||
- encoding
|
||||
- react
|
||||
- react-devtools-core
|
||||
- react-dom
|
||||
- supports-color
|
||||
- ts-node
|
||||
- typescript
|
||||
@@ -3739,6 +3741,7 @@ snapshots:
|
||||
js-yaml: 4.1.0
|
||||
mdast-util-mdx-jsx: 3.2.0
|
||||
neotraverse: 0.6.18
|
||||
openapi-types: 12.1.3
|
||||
puppeteer: 22.15.0(typescript@5.9.2)
|
||||
rehype-parse: 9.0.1
|
||||
remark-gfm: 4.0.1
|
||||
|
||||
@@ -5,7 +5,7 @@ let
|
||||
submodule = "examples/${name}";
|
||||
|
||||
node_modules = nixops-lib.js.mkNodeModules {
|
||||
name = "node-modules";
|
||||
name = "node-modules-${name}";
|
||||
version = "0.0.0-dev";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
@@ -71,8 +71,6 @@ in
|
||||
] ++ checkDeps ++ buildInputs ++ nativeBuildInputs;
|
||||
};
|
||||
|
||||
entrypoint = pkgs.writeScriptBin "docker-entrypoint.sh" (builtins.readFile ./docker-entrypoint.sh);
|
||||
|
||||
check = nixops-lib.js.check {
|
||||
inherit src node_modules submodule buildInputs nativeBuildInputs checkDeps;
|
||||
|
||||
|
||||
17
examples/guides/Makefile
Normal file
17
examples/guides/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
ROOT_DIR?=$(abspath ../..)
|
||||
include $(ROOT_DIR)/build/makefiles/general.makefile
|
||||
|
||||
|
||||
.PHONY: _dev-env-up
|
||||
_dev-env-up:
|
||||
cd ../demos/backend/ && $(ROOT_DIR)/examples/demos/backend/env-up.sh
|
||||
|
||||
|
||||
.PHONY: _dev-env-down
|
||||
_dev-env-down:
|
||||
cd ../demos/backend/ && nhost down --volumes
|
||||
|
||||
|
||||
.PHONY: _dev-env-build
|
||||
_dev-env-build:
|
||||
@echo "Nothing to do"
|
||||
13
examples/guides/package.json
Normal file
13
examples/guides/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "guides",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "pnpm run --sequential --filter './**' build",
|
||||
"test": "pnpm run --sequential --filter './**' test",
|
||||
"generate": "pnpm run --sequential --filter './**' generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nhost/nhost-js": "workspace:^"
|
||||
}
|
||||
}
|
||||
13
examples/guides/pnpm-lock.yaml
generated
Normal file
13
examples/guides/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@nhost/nhost-js':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/nhost-js
|
||||
105
examples/guides/project.nix
Normal file
105
examples/guides/project.nix
Normal file
@@ -0,0 +1,105 @@
|
||||
{ self, pkgs, nix2containerPkgs, nix-filter, nixops-lib }:
|
||||
let
|
||||
name = "guides";
|
||||
version = "0.0.0-dev";
|
||||
submodule = "examples/${name}";
|
||||
|
||||
node_modules = nixops-lib.js.mkNodeModules {
|
||||
name = "node-modules-${name}";
|
||||
version = "0.0.0-dev";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ../..;
|
||||
include = [
|
||||
".npmrc "
|
||||
"package.json"
|
||||
"pnpm-workspace.yaml "
|
||||
"pnpm-lock.yaml"
|
||||
"${submodule}/package.json"
|
||||
"${submodule}/pnpm-lock.yaml"
|
||||
"${submodule}/react-apollo/package.json"
|
||||
"${submodule}/react-apollo/pnpm-lock.yaml"
|
||||
"${submodule}/react-query/package.json"
|
||||
"${submodule}/react-query/pnpm-lock.yaml"
|
||||
];
|
||||
};
|
||||
|
||||
pnpmOpts = "--filter . --filter './${submodule}/**'";
|
||||
|
||||
preBuild = ''
|
||||
mkdir packages
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
'';
|
||||
};
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ../../.;
|
||||
include = with nix-filter.lib; [
|
||||
isDirectory
|
||||
".gitignore"
|
||||
".npmrc"
|
||||
"audit-ci.jsonc"
|
||||
"biome.json"
|
||||
"package.json"
|
||||
"pnpm-workspace.yaml"
|
||||
"pnpm-lock.yaml"
|
||||
"turbo.json"
|
||||
(inDirectory "./build")
|
||||
(inDirectory "${submodule}")
|
||||
];
|
||||
};
|
||||
|
||||
checkDeps = with pkgs; [ nhost-cli biome ];
|
||||
|
||||
buildInputs = with pkgs; [ nodejs ];
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pnpm cacert ];
|
||||
in
|
||||
{
|
||||
devShell = nixops-lib.js.devShell {
|
||||
inherit node_modules;
|
||||
|
||||
buildInputs = [
|
||||
] ++ checkDeps ++ buildInputs ++ nativeBuildInputs;
|
||||
};
|
||||
|
||||
check = nixops-lib.js.check {
|
||||
inherit src node_modules submodule buildInputs nativeBuildInputs checkDeps;
|
||||
|
||||
preCheck = ''
|
||||
rm -rf packages/nhost-js
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
'';
|
||||
};
|
||||
|
||||
package = pkgs.stdenv.mkDerivation {
|
||||
inherit name version src;
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pnpm cacert nodejs ];
|
||||
buildInputs = with pkgs; [ nodejs ];
|
||||
|
||||
buildPhase = ''
|
||||
mkdir -p $TMPDIR/home
|
||||
export HOME=$TMPDIR/home
|
||||
|
||||
chmod +w -R .
|
||||
|
||||
for absdir in $(pnpm list --recursive --depth=-1 --parseable); do
|
||||
dir=$(realpath --relative-to="$PWD" "$absdir")
|
||||
echo "➜ Copying node_modules for $dir"
|
||||
cp -r ${node_modules}/$dir/node_modules $dir/node_modules
|
||||
done
|
||||
|
||||
rm -rf packages/nhost-js
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
|
||||
cd ${submodule}
|
||||
|
||||
pnpm build
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
'';
|
||||
};
|
||||
}
|
||||
25
examples/guides/react-apollo/.gitignore
vendored
Normal file
25
examples/guides/react-apollo/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vite
|
||||
391
examples/guides/react-apollo/README.md
Normal file
391
examples/guides/react-apollo/README.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# React Apollo with Nhost SDK
|
||||
|
||||
This guide demonstrates how to integrate GraphQL queries and mutations with React using Apollo Client and the Nhost SDK.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install @apollo/client @nhost/nhost-js graphql
|
||||
# or
|
||||
yarn add @apollo/client @nhost/nhost-js graphql
|
||||
# or
|
||||
pnpm add @apollo/client @nhost/nhost-js graphql
|
||||
```
|
||||
|
||||
### 2. Generate Types with GraphQL CodeGen
|
||||
|
||||
Install GraphQL CodeGen:
|
||||
|
||||
```bash
|
||||
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
|
||||
```
|
||||
|
||||
Set up `codegen.ts`:
|
||||
|
||||
```typescript
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
"https://local.graphql.local.nhost.run/v1": {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
documents: ["src/**/*.ts"],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
"./src/lib/graphql/__generated__/graphql.ts": {
|
||||
documents: ["src/lib/graphql/**/*.graphql"],
|
||||
plugins: [
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
"typescript-react-apollo",
|
||||
],
|
||||
config: {
|
||||
scalars: {
|
||||
UUID: "string",
|
||||
uuid: "string",
|
||||
timestamptz: "string",
|
||||
jsonb: "Record<string, any>",
|
||||
bigint: "number",
|
||||
bytea: "Buffer",
|
||||
citext: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"./schema.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
config: {
|
||||
includeDirectives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### 1. Create an Auth Provider
|
||||
|
||||
Create an authentication context to manage the user session:
|
||||
|
||||
```typescript
|
||||
// src/lib/nhost/AuthProvider.tsx
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { createClient, type NhostClient } from "@nhost/nhost-js";
|
||||
import { type Session } from "@nhost/nhost-js/auth";
|
||||
|
||||
interface AuthContextType {
|
||||
user: Session["user"] | null;
|
||||
session: Session | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
nhost: NhostClient;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<Session["user"] | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
|
||||
// Create the nhost client
|
||||
const nhost = useMemo(
|
||||
() =>
|
||||
createClient({
|
||||
region: import.meta.env.VITE_NHOST_REGION || "local",
|
||||
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
setIsLoading(false);
|
||||
|
||||
const unsubscribe = nhost.sessionStorage.onChange((currentSession) => {
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [nhost]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
nhost,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Create Apollo Client Integration
|
||||
|
||||
Configure Apollo Client with the Nhost authentication:
|
||||
|
||||
```typescript
|
||||
// src/lib/graphql/apolloClient.ts
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
createHttpLink,
|
||||
ApolloLink,
|
||||
} from "@apollo/client";
|
||||
import { setContext } from "@apollo/client/link/context";
|
||||
import { useAuth } from "../nhost/AuthProvider";
|
||||
import { useMemo } from "react";
|
||||
import type { NhostClient } from "@nhost/nhost-js";
|
||||
|
||||
export const createApolloClient = (nhost: NhostClient) => {
|
||||
const httpLink = createHttpLink({
|
||||
uri: nhost.graphql.url,
|
||||
});
|
||||
|
||||
const authLink = setContext(async (_, prevContext) => {
|
||||
const resp = await nhost.refreshSession(60);
|
||||
const token = resp ? resp.accessToken : null;
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...(prevContext["headers"] as Record<string, string>),
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const link = ApolloLink.from([authLink, httpLink]);
|
||||
|
||||
return new ApolloClient({
|
||||
link,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useApolloClient = () => {
|
||||
const { nhost } = useAuth();
|
||||
return useMemo(() => createApolloClient(nhost), [nhost]);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Set Up Apollo Provider
|
||||
|
||||
Wrap your application with the Apollo Provider:
|
||||
|
||||
```typescript
|
||||
// src/main.tsx or src/App.tsx
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import App from "./App";
|
||||
import { AuthProvider } from "./lib/nhost/AuthProvider";
|
||||
import { useApolloClient } from "./lib/graphql/apolloClient";
|
||||
|
||||
const AppWithProviders = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
return (
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<AppWithProviders />
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Define GraphQL Operations
|
||||
|
||||
Create a GraphQL file with your queries and mutations:
|
||||
|
||||
```graphql
|
||||
# src/lib/graphql/operations.graphql
|
||||
query GetNinjaTurtlesWithComments {
|
||||
ninjaTurtles {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
comments {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
email
|
||||
displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
|
||||
insert_comments_one(
|
||||
object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Generate TypeScript Types
|
||||
|
||||
Run the code generator:
|
||||
|
||||
```bash
|
||||
npx graphql-codegen
|
||||
```
|
||||
|
||||
### 6. Use in Components
|
||||
|
||||
Use the generated hooks in your components:
|
||||
|
||||
```tsx
|
||||
// src/pages/Home.tsx
|
||||
import { useState } from "react";
|
||||
import {
|
||||
useGetNinjaTurtlesWithCommentsQuery,
|
||||
useAddCommentMutation,
|
||||
} from "../lib/graphql/__generated__/graphql";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Home() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
|
||||
// Query for data
|
||||
const { loading, error, data, refetch } =
|
||||
useGetNinjaTurtlesWithCommentsQuery();
|
||||
|
||||
// Mutation hook
|
||||
const [addComment] = useAddCommentMutation({
|
||||
onCompleted: () => {
|
||||
setCommentText("");
|
||||
setActiveCommentId(null);
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <div>Please sign in</div>;
|
||||
}
|
||||
|
||||
const handleAddComment = (turtleId: string) => {
|
||||
if (!commentText.trim()) return;
|
||||
|
||||
addComment({
|
||||
variables: {
|
||||
ninjaTurtleId: turtleId,
|
||||
comment: commentText,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const ninjaTurtles = data?.ninjaTurtles || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Ninja Turtles</h1>
|
||||
{ninjaTurtles.map((turtle) => (
|
||||
<div key={turtle.id}>
|
||||
<h2>{turtle.name}</h2>
|
||||
<p>{turtle.description}</p>
|
||||
|
||||
{/* Comments section */}
|
||||
<div>
|
||||
<h3>Comments ({turtle.comments.length})</h3>
|
||||
|
||||
{turtle.comments.map((comment) => (
|
||||
<div key={comment.id}>
|
||||
<p>{comment.comment}</p>
|
||||
<small>
|
||||
By{" "}
|
||||
{comment.user?.displayName ||
|
||||
comment.user?.email ||
|
||||
"Anonymous"}
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeCommentId === turtle.id ? (
|
||||
<div>
|
||||
<textarea
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Add your comment..."
|
||||
/>
|
||||
<div>
|
||||
<button onClick={() => setActiveCommentId(null)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={() => handleAddComment(turtle.id)}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setActiveCommentId(turtle.id)}>
|
||||
Add a comment
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
7
examples/guides/react-apollo/biome.json
Normal file
7
examples/guides/react-apollo/biome.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"linter": {
|
||||
"includes": ["**", "!src/lib/graphql/__generated__/graphql.ts"]
|
||||
}
|
||||
}
|
||||
27
examples/guides/react-apollo/codegen-wrapper.sh
Executable file
27
examples/guides/react-apollo/codegen-wrapper.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Running GraphQL code generator..."
|
||||
pnpm graphql-codegen --config codegen.ts
|
||||
|
||||
GENERATED_TS_FILE="src/lib/graphql/__generated__/graphql.ts"
|
||||
GENERATED_SCHEMA_FILE="schema.graphql"
|
||||
|
||||
if [ -f "$GENERATED_TS_FILE" ]; then
|
||||
echo "Formatting $GENERATED_TS_FILE..."
|
||||
biome check --write "$GENERATED_TS_FILE"
|
||||
echo "Successfully formatted $GENERATED_TS_FILE"
|
||||
else
|
||||
echo "Error: Generated TypeScript file not found at $GENERATED_TS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$GENERATED_SCHEMA_FILE" ]; then
|
||||
echo "Formatting $GENERATED_SCHEMA_FILE..."
|
||||
biome check --write "$GENERATED_SCHEMA_FILE"
|
||||
echo "Successfully formatted $GENERATED_SCHEMA_FILE"
|
||||
else
|
||||
echo "Warning: Generated schema file not found at $GENERATED_SCHEMA_FILE"
|
||||
fi
|
||||
|
||||
echo "All tasks completed successfully."
|
||||
44
examples/guides/react-apollo/codegen.ts
Normal file
44
examples/guides/react-apollo/codegen.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
"https://local.graphql.local.nhost.run/v1": {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
documents: ["src/**/*.ts"],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
"./src/lib/graphql/__generated__/graphql.ts": {
|
||||
documents: ["src/lib/graphql/**/*.graphql"],
|
||||
plugins: [
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
"typescript-react-apollo",
|
||||
],
|
||||
config: {
|
||||
scalars: {
|
||||
UUID: "string",
|
||||
uuid: "string",
|
||||
timestamptz: "string",
|
||||
jsonb: "Record<string, any>",
|
||||
bigint: "number",
|
||||
bytea: "Buffer",
|
||||
citext: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"./schema.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
config: {
|
||||
includeDirectives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
13
examples/guides/react-apollo/index.html
Normal file
13
examples/guides/react-apollo/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
36
examples/guides/react-apollo/package.json
Normal file
36
examples/guides/react-apollo/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "demos/react-apollo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"generate": "bash codegen-wrapper.sh",
|
||||
"test": "pnpm test:typecheck && pnpm test:lint",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"test:lint": "biome check",
|
||||
"format": "biome format --write",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@nhost/nhost-js": "workspace:*",
|
||||
"graphql": "^16.11.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.6",
|
||||
"@graphql-codegen/schema-ast": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^4.1.6",
|
||||
"@graphql-codegen/typescript-operations": "^4.6.1",
|
||||
"@graphql-codegen/typescript-react-apollo": "^4.3.3",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1"
|
||||
}
|
||||
}
|
||||
4665
examples/guides/react-apollo/pnpm-lock.yaml
generated
Normal file
4665
examples/guides/react-apollo/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
examples/guides/react-apollo/public/vite.svg
Normal file
1
examples/guides/react-apollo/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
10143
examples/guides/react-apollo/schema.graphql
Normal file
10143
examples/guides/react-apollo/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
56
examples/guides/react-apollo/src/App.tsx
Normal file
56
examples/guides/react-apollo/src/App.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { JSX } from "react";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import Navigation from "./components/Navigation";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import Home from "./pages/Home";
|
||||
import Profile from "./pages/Profile";
|
||||
import SignIn from "./pages/SignIn";
|
||||
import SignUp from "./pages/SignUp";
|
||||
|
||||
// Root layout component to wrap all routes
|
||||
const RootLayout = (): JSX.Element => {
|
||||
return (
|
||||
<div className="flex-col min-h-screen">
|
||||
<Navigation />
|
||||
<main className="max-w-2xl mx-auto p-6 w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
<footer>
|
||||
<p
|
||||
className="text-sm text-center"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
© {new Date().getFullYear()} Nhost Demo
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Create router with routes
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route element={<RootLayout />}>
|
||||
<Route path="signin" element={<SignIn />} />
|
||||
<Route path="signup" element={<SignUp />} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="home" element={<Home />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Route>,
|
||||
),
|
||||
);
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
85
examples/guides/react-apollo/src/components/Navigation.tsx
Normal file
85
examples/guides/react-apollo/src/components/Navigation.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { JSX } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Navigation(): JSX.Element {
|
||||
const { isAuthenticated, nhost, session } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
// Helper function to determine if a link is active
|
||||
const isActive = (path: string): string => {
|
||||
return location.pathname === path ? "active" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-container">
|
||||
<div className="flex items-center">
|
||||
<span className="navbar-brand">Nhost Demo</span>
|
||||
<div className="navbar-links">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link to="/home" className={`nav-link ${isActive("/home")}`}>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
className={`nav-link ${isActive("/profile")}`}
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/signin"
|
||||
className={`nav-link ${isActive("/signin")}`}
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup"
|
||||
className={`nav-link ${isActive("/signup")}`}
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAuthenticated && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (session) {
|
||||
await nhost.auth.signOut({
|
||||
refreshToken: session.refreshToken,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="icon-button"
|
||||
title="Sign Out"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
role="img"
|
||||
aria-label="Sign out"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({
|
||||
redirectTo = "/signin",
|
||||
}: ProtectedRouteProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to={redirectTo} />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
552
examples/guides/react-apollo/src/index.css
Normal file
552
examples/guides/react-apollo/src/index.css
Normal file
@@ -0,0 +1,552 @@
|
||||
/* Base styles */
|
||||
:root {
|
||||
--background: #030712;
|
||||
--foreground: #ffffff;
|
||||
--card-bg: #111827;
|
||||
--card-border: #1f2937;
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--secondary: #10b981;
|
||||
--secondary-hover: #059669;
|
||||
--accent: #8b5cf6;
|
||||
--accent-hover: #7c3aed;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-color: rgba(31, 41, 55, 0.7);
|
||||
--font-geist-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.space-y-5 > * + * {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.space-x-4 > * + * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, var(--primary), var(--accent));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
.glass-card {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.625rem 1rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: var(--secondary-hover);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: white;
|
||||
background-color: rgba(31, 41, 55, 0.7);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
border: 1px solid var(--border-color);
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid rgba(34, 197, 94, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(17, 24, 39, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
font-size: 1.125rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(31, 41, 55, 0.3);
|
||||
}
|
||||
|
||||
/* File upload styles */
|
||||
.file-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
border: 2px dashed rgba(99, 102, 241, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
background-color: rgba(31, 41, 55, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-upload:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 1.25rem 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Profile data */
|
||||
.profile-item {
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.profile-item strong {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
margin-right: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-link-danger {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-link-danger:hover {
|
||||
color: #f05252;
|
||||
}
|
||||
|
||||
/* Icon button */
|
||||
.icon-button {
|
||||
background-color: transparent;
|
||||
color: var(--primary);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.icon-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Table action icons */
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.action-icon:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-icon-view {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-icon-view:hover:not(:disabled) {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.action-icon-delete {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-icon-delete:hover:not(:disabled) {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #f05252;
|
||||
}
|
||||
|
||||
/* Tab styles */
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
background-color: rgba(31, 41, 55, 0.5);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tab-button:hover:not(.tab-active) {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-button.tab-active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.tab-button:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
6994
examples/guides/react-apollo/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
6994
examples/guides/react-apollo/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
examples/guides/react-apollo/src/lib/graphql/apolloClient.ts
Normal file
53
examples/guides/react-apollo/src/lib/graphql/apolloClient.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
createHttpLink,
|
||||
InMemoryCache,
|
||||
} from "@apollo/client";
|
||||
import { setContext } from "@apollo/client/link/context";
|
||||
import type { NhostClient } from "@nhost/nhost-js";
|
||||
import { useMemo } from "react";
|
||||
import { useAuth } from "../nhost/AuthProvider";
|
||||
|
||||
// Create a function that creates an Apollo client using the provided Nhost client
|
||||
export const createApolloClient = (nhost: NhostClient) => {
|
||||
// HTTP connection to the API
|
||||
const httpLink = createHttpLink({
|
||||
uri: nhost.graphql.url,
|
||||
});
|
||||
// Auth link to append headers
|
||||
const authLink = setContext(async (_, prevContext) => {
|
||||
// Get the authentication token from nhost
|
||||
const resp = await nhost.refreshSession(60);
|
||||
const token = resp ? resp.accessToken : null;
|
||||
|
||||
// Return the headers to the context so httpLink can read them
|
||||
return {
|
||||
headers: {
|
||||
...(prevContext["headers"] as Record<string, string>),
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Create ApolloLink instance
|
||||
const link = ApolloLink.from([authLink, httpLink]);
|
||||
|
||||
// Create and return Apollo client
|
||||
return new ApolloClient({
|
||||
link,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Hook to get the Apollo client using the nhost client from AuthProvider
|
||||
export const useApolloClient = () => {
|
||||
const { nhost } = useAuth();
|
||||
|
||||
return useMemo(() => createApolloClient(nhost), [nhost]);
|
||||
};
|
||||
28
examples/guides/react-apollo/src/lib/graphql/queries.graphql
Normal file
28
examples/guides/react-apollo/src/lib/graphql/queries.graphql
Normal file
@@ -0,0 +1,28 @@
|
||||
query GetNinjaTurtlesWithComments {
|
||||
ninjaTurtles {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
comments {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
|
||||
insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
ninjaTurtleId
|
||||
}
|
||||
}
|
||||
175
examples/guides/react-apollo/src/lib/nhost/AuthProvider.tsx
Normal file
175
examples/guides/react-apollo/src/lib/nhost/AuthProvider.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { createClient, type NhostClient } from "@nhost/nhost-js";
|
||||
import type { Session } from "@nhost/nhost-js/auth";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* Authentication context interface providing access to user session state and Nhost client.
|
||||
* Used throughout the React application to access authentication-related data and operations.
|
||||
*/
|
||||
interface AuthContextType {
|
||||
/** Current authenticated user object, null if not authenticated */
|
||||
user: Session["user"] | null;
|
||||
/** Current session object containing tokens and user data, null if no active session */
|
||||
session: Session | null;
|
||||
/** Boolean indicating if user is currently authenticated */
|
||||
isAuthenticated: boolean;
|
||||
/** Boolean indicating if authentication state is still loading */
|
||||
isLoading: boolean;
|
||||
/** Nhost client instance for making authenticated requests */
|
||||
nhost: NhostClient;
|
||||
}
|
||||
|
||||
// Create React context for authentication state and nhost client
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthProvider component that provides authentication context to the React application.
|
||||
*
|
||||
* This component handles:
|
||||
* - Initializing the Nhost client with default EventEmitterStorage
|
||||
* - Managing authentication state (user, session, loading, authenticated status)
|
||||
* - Cross-tab session synchronization using sessionStorage.onChange events
|
||||
* - Page visibility and focus event handling to maintain session consistency
|
||||
* - Client-side only session management (no server-side rendering)
|
||||
*/
|
||||
export const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const [user, setUser] = useState<Session["user"] | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const lastRefreshTokenIdRef = useRef<string | null>(null);
|
||||
|
||||
// Initialize Nhost client with default SessionStorage (local storage)
|
||||
const nhost = useMemo(
|
||||
() =>
|
||||
createClient({
|
||||
region: import.meta.env.VITE_NHOST_REGION || "local",
|
||||
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles session reload when refresh token changes.
|
||||
* This detects when the session has been updated from other tabs.
|
||||
* Unlike the Next.js version, this only updates local state without server synchronization.
|
||||
*
|
||||
* @param currentRefreshTokenId - The current refresh token ID to compare against stored value
|
||||
*/
|
||||
const reloadSession = useCallback(
|
||||
(currentRefreshTokenId: string | null) => {
|
||||
if (currentRefreshTokenId !== lastRefreshTokenIdRef.current) {
|
||||
lastRefreshTokenIdRef.current = currentRefreshTokenId;
|
||||
|
||||
// Update local authentication state to match current session
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
}
|
||||
},
|
||||
[nhost],
|
||||
);
|
||||
|
||||
// Initialize authentication state and set up cross-tab session synchronization
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Load initial session state from Nhost client
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
lastRefreshTokenIdRef.current = currentSession?.refreshTokenId ?? null;
|
||||
setIsLoading(false);
|
||||
|
||||
// Subscribe to session changes from other browser tabs
|
||||
// This enables real-time synchronization when user signs in/out in another tab
|
||||
const unsubscribe = nhost.sessionStorage.onChange((session) => {
|
||||
reloadSession(session?.refreshTokenId ?? null);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [nhost, reloadSession]);
|
||||
|
||||
// Handle session changes from page focus events (for additional session consistency)
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Checks for session changes when page becomes visible or focused.
|
||||
* In the React SPA context, this provides additional consistency checks
|
||||
* though it's less critical than in the Next.js SSR version.
|
||||
*/
|
||||
const checkSessionOnFocus = () => {
|
||||
reloadSession(nhost.getUserSession()?.refreshTokenId ?? null);
|
||||
};
|
||||
|
||||
// Monitor page visibility changes (tab switching, window minimizing)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) {
|
||||
checkSessionOnFocus();
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor window focus events (clicking back into the browser window)
|
||||
window.addEventListener("focus", checkSessionOnFocus);
|
||||
|
||||
// Cleanup event listeners on component unmount
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", checkSessionOnFocus);
|
||||
window.removeEventListener("focus", checkSessionOnFocus);
|
||||
};
|
||||
}, [nhost, reloadSession]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
nhost,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to access the authentication context.
|
||||
*
|
||||
* Must be used within a component wrapped by AuthProvider.
|
||||
* Provides access to current user session, authentication state, and Nhost client.
|
||||
*
|
||||
* @throws {Error} When used outside of AuthProvider
|
||||
* @returns {AuthContextType} Authentication context containing user, session, and client
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { user, isAuthenticated, nhost } = useAuth();
|
||||
*
|
||||
* if (!isAuthenticated) {
|
||||
* return <div>Please sign in</div>;
|
||||
* }
|
||||
*
|
||||
* return <div>Welcome, {user?.displayName}!</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
33
examples/guides/react-apollo/src/main.tsx
Normal file
33
examples/guides/react-apollo/src/main.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import App from "./App";
|
||||
import { useApolloClient } from "./lib/graphql/apolloClient";
|
||||
import { AuthProvider } from "./lib/nhost/AuthProvider";
|
||||
|
||||
// Wrapper component that provides Apollo client using the Nhost client from AuthProvider
|
||||
const ApolloProviderWithAuth = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>;
|
||||
};
|
||||
|
||||
// Root component that sets up providers
|
||||
const Root = () => (
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<ApolloProviderWithAuth>
|
||||
<App />
|
||||
</ApolloProviderWithAuth>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) throw new Error("Root element not found");
|
||||
|
||||
createRoot(rootElement).render(<Root />);
|
||||
217
examples/guides/react-apollo/src/pages/Home.css
Normal file
217
examples/guides/react-apollo/src/pages/Home.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/* Custom styles for Ninja Turtles tabs interface */
|
||||
.ninja-turtles-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ninja-turtles-title {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
color: #1a9c44;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Tab navigation */
|
||||
.turtle-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #1a9c44;
|
||||
margin-bottom: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.turtle-tab {
|
||||
padding: 10px 20px;
|
||||
margin-right: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.turtle-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(26, 156, 68, 0.1);
|
||||
}
|
||||
|
||||
.turtle-tab.active {
|
||||
color: white;
|
||||
background: #1a9c44;
|
||||
}
|
||||
|
||||
/* Turtle Card Styles */
|
||||
.turtle-card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.turtle-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.turtle-name {
|
||||
color: #1a9c44;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.turtle-description {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.turtle-date {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Comments section */
|
||||
.comments-section {
|
||||
margin-top: 25px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
color: #1a9c44;
|
||||
}
|
||||
|
||||
.comment-card {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(26, 156, 68, 0.05);
|
||||
border: 1px solid rgba(26, 156, 68, 0.1);
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #1a9c44;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Comment form */
|
||||
.comment-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comment-textarea {
|
||||
width: 100%;
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-textarea:focus {
|
||||
border-color: #1a9c44;
|
||||
box-shadow: 0 0 0 2px rgba(26, 156, 68, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #1a9c44;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #148035;
|
||||
}
|
||||
|
||||
.add-comment-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #1a9c44;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px 0;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.add-comment-button:hover {
|
||||
color: #148035;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.add-comment-button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.turtle-tabs {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.turtle-tab {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
203
examples/guides/react-apollo/src/pages/Home.tsx
Normal file
203
examples/guides/react-apollo/src/pages/Home.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { type JSX, useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import {
|
||||
useAddCommentMutation,
|
||||
useGetNinjaTurtlesWithCommentsQuery,
|
||||
} from "../lib/graphql/__generated__/graphql";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
import "./Home.css";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
|
||||
const { loading, error, data, refetch } =
|
||||
useGetNinjaTurtlesWithCommentsQuery();
|
||||
|
||||
const [addComment] = useAddCommentMutation({
|
||||
onCompleted: () => {
|
||||
setCommentText("");
|
||||
setActiveCommentId(null);
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
// If authentication is still loading, show a loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If not authenticated, redirect to signin page
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/signin" />;
|
||||
}
|
||||
|
||||
const handleAddComment = (turtleId: string) => {
|
||||
if (!commentText.trim()) return;
|
||||
|
||||
addComment({
|
||||
variables: {
|
||||
ninjaTurtleId: turtleId,
|
||||
comment: commentText,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading ninja turtles...</p>
|
||||
</div>
|
||||
);
|
||||
if (error)
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
Error loading ninja turtles: {error.message}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Access the data using the correct field name from the GraphQL response
|
||||
const ninjaTurtles = data?.ninjaTurtles || [];
|
||||
if (!ninjaTurtles || ninjaTurtles.length === 0) {
|
||||
return (
|
||||
<div className="no-turtles-container">
|
||||
<p>No ninja turtles found. Please add some!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Set the active tab to the first turtle if there's no active tab and there are turtles
|
||||
if (activeTabId === null) {
|
||||
setActiveTabId(ninjaTurtles[0] ? ninjaTurtles[0].id : null);
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ninja-turtles-container">
|
||||
<h1 className="ninja-turtles-title text-3xl font-bold mb-6">
|
||||
Teenage Mutant Ninja Turtles
|
||||
</h1>
|
||||
|
||||
{/* Tabs navigation */}
|
||||
<div className="turtle-tabs">
|
||||
{ninjaTurtles.map((turtle) => (
|
||||
<button
|
||||
type="button"
|
||||
key={turtle.id}
|
||||
className={`turtle-tab ${activeTabId === turtle.id ? "active" : ""}`}
|
||||
onClick={() => setActiveTabId(turtle.id)}
|
||||
>
|
||||
{turtle.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Display active turtle */}
|
||||
{ninjaTurtles
|
||||
.filter((turtle) => turtle.id === activeTabId)
|
||||
.map((turtle) => (
|
||||
<div key={turtle.id} className="turtle-card glass-card p-6">
|
||||
<div className="turtle-header">
|
||||
<h2 className="turtle-name text-2xl font-semibold">
|
||||
{turtle.name}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="turtle-description">{turtle.description}</p>
|
||||
|
||||
<div className="turtle-date">
|
||||
Added on {formatDate(turtle.createdAt || turtle.createdAt)}
|
||||
</div>
|
||||
|
||||
<div className="comments-section">
|
||||
<h3 className="comments-title">
|
||||
Comments ({turtle.comments.length})
|
||||
</h3>
|
||||
|
||||
{turtle.comments.map((comment) => (
|
||||
<div key={comment.id} className="comment-card">
|
||||
<p className="comment-text">{comment.comment}</p>
|
||||
<div className="comment-meta">
|
||||
<div className="comment-avatar">
|
||||
{(comment.user?.displayName || comment.user?.email || "?")
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<p>
|
||||
{comment.user?.displayName ||
|
||||
comment.user?.email ||
|
||||
"Anonymous"}{" "}
|
||||
- {formatDate(comment.createdAt || comment.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeCommentId === turtle.id ? (
|
||||
<div className="comment-form">
|
||||
<textarea
|
||||
className="comment-textarea"
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Add your comment..."
|
||||
rows={3}
|
||||
/>
|
||||
<div className="comment-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveCommentId(null);
|
||||
setCommentText("");
|
||||
}}
|
||||
className="btn cancel-button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddComment(turtle.id)}
|
||||
className="btn submit-button"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveCommentId(turtle.id)}
|
||||
className="add-comment-button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
role="img"
|
||||
aria-label="Add comment"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
Add a comment
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
examples/guides/react-apollo/src/pages/Profile.tsx
Normal file
66
examples/guides/react-apollo/src/pages/Profile.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { JSX } from "react";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Profile(): JSX.Element {
|
||||
const { user, session } = useAuth();
|
||||
|
||||
// ProtectedRoute component now handles authentication check
|
||||
// We can just focus on the component logic here
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Your Profile</h1>
|
||||
|
||||
<div className="glass-card p-8 mb-6">
|
||||
<div className="space-y-5">
|
||||
<div className="profile-item">
|
||||
<strong>Display Name:</strong>
|
||||
<span className="ml-2">{user?.displayName || "Not set"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Email:</strong>
|
||||
<span className="ml-2">{user?.email || "Not available"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>User ID:</strong>
|
||||
<span
|
||||
className="ml-2"
|
||||
style={{
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{user?.id || "Not available"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Roles:</strong>
|
||||
<span className="ml-2">{user?.roles?.join(", ") || "None"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Email Verified:</strong>
|
||||
<span className="ml-2">{user?.emailVerified ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-8 mb-6">
|
||||
<h3 className="text-xl mb-4">Session Information</h3>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
refreshTokenId: session?.refreshTokenId,
|
||||
accessTokenExpiresIn: session?.accessTokenExpiresIn,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
examples/guides/react-apollo/src/pages/SignIn.tsx
Normal file
120
examples/guides/react-apollo/src/pages/SignIn.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { ErrorResponse } from "@nhost/nhost-js/auth";
|
||||
import type { FetchError } from "@nhost/nhost-js/fetch";
|
||||
import { type JSX, useEffect, useId, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function SignIn(): JSX.Element {
|
||||
const { nhost, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(
|
||||
params.get("error") || null,
|
||||
);
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
const isVerifying = params.has("fromVerify");
|
||||
|
||||
// Use useEffect for navigation after authentication is confirmed
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !isVerifying) {
|
||||
navigate("/home");
|
||||
}
|
||||
}, [isAuthenticated, isVerifying, navigate]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Use the signIn function from auth context
|
||||
const response = await nhost.auth.signInEmailPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// Check if MFA is required
|
||||
if (response.body?.mfa) {
|
||||
navigate(`/signin/mfa?ticket=${response.body.mfa.ticket}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a session, sign in was successful
|
||||
if (response.body?.session) {
|
||||
navigate("/home");
|
||||
} else {
|
||||
setError("Failed to sign in");
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as FetchError<ErrorResponse>;
|
||||
setError(`An error occurred during sign in: ${error.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
|
||||
|
||||
<div className="glass-card w-full p-8 mb-6">
|
||||
<h2 className="text-2xl mb-6">Sign In</h2>
|
||||
<div>
|
||||
<div className="tabs-container">
|
||||
<button type="button" className="tab-button tab-active">
|
||||
Email + Password
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor={emailId}>Email</label>
|
||||
<input
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={passwordId}>Password</label>
|
||||
<input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing In..." : "Sign In"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p>
|
||||
Don't have an account? <Link to="/signup">Sign Up</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
examples/guides/react-apollo/src/pages/SignUp.tsx
Normal file
127
examples/guides/react-apollo/src/pages/SignUp.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { ErrorResponse } from "@nhost/nhost-js/auth";
|
||||
import type { FetchError } from "@nhost/nhost-js/fetch";
|
||||
import { type JSX, useId, useState } from "react";
|
||||
import { Link, Navigate, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function SignUp(): JSX.Element {
|
||||
const { nhost, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [displayName, setDisplayName] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const displayNameId = useId();
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
// If already authenticated, redirect to profile
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/home" />;
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await nhost.auth.signUpEmailPassword({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
displayName,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body) {
|
||||
// Successfully signed up and automatically signed in
|
||||
navigate("/home");
|
||||
} else {
|
||||
// Verification email sent
|
||||
navigate("/verify");
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as FetchError<ErrorResponse>;
|
||||
setError(`An error occurred during sign up: ${error.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
|
||||
|
||||
<div className="glass-card w-full p-8 mb-6">
|
||||
<h2 className="text-2xl mb-6">Sign Up</h2>
|
||||
|
||||
<div>
|
||||
<div className="tabs-container">
|
||||
<button type="button" className="tab-button tab-active">
|
||||
Email + Password
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor={displayNameId}>Display Name</label>
|
||||
<input
|
||||
id={displayNameId}
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={emailId}>Email</label>
|
||||
<input
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={passwordId}>Password</label>
|
||||
<input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs mt-1 text-gray-400">
|
||||
Password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing Up..." : "Sign Up"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p>
|
||||
Already have an account? <Link to="/signin">Sign In</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
examples/guides/react-apollo/src/vite-env.d.ts
vendored
Normal file
11
examples/guides/react-apollo/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_NHOST_REGION: string | undefined;
|
||||
readonly VITE_NHOST_SUBDOMAIN: string | undefined;
|
||||
readonly VITE_ENV: string | undefined;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
6
examples/guides/react-apollo/tsconfig.json
Normal file
6
examples/guides/react-apollo/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/frontend.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
4
examples/guides/react-apollo/tsconfig.node.json
Normal file
4
examples/guides/react-apollo/tsconfig.node.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/vite.json"
|
||||
}
|
||||
7
examples/guides/react-apollo/vite.config.ts
Normal file
7
examples/guides/react-apollo/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
25
examples/guides/react-query/.gitignore
vendored
Normal file
25
examples/guides/react-query/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vite
|
||||
493
examples/guides/react-query/README.md
Normal file
493
examples/guides/react-query/README.md
Normal file
@@ -0,0 +1,493 @@
|
||||
# React Query with Nhost SDK
|
||||
|
||||
This guide demonstrates how to integrate GraphQL queries and mutations with React using TanStack Query (React Query) and the Nhost SDK.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install @tanstack/react-query @nhost/nhost-js graphql
|
||||
# or
|
||||
yarn add @tanstack/react-query @nhost/nhost-js graphql
|
||||
# or
|
||||
pnpm add @tanstack/react-query @nhost/nhost-js graphql
|
||||
```
|
||||
|
||||
For development, add React Query DevTools:
|
||||
|
||||
```bash
|
||||
npm install -D @tanstack/react-query-devtools
|
||||
```
|
||||
|
||||
### 2. Generate Types with GraphQL CodeGen
|
||||
|
||||
Install GraphQL CodeGen:
|
||||
|
||||
```bash
|
||||
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/schema-ast
|
||||
```
|
||||
|
||||
Set up `codegen.ts`:
|
||||
|
||||
```typescript
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
"https://local.graphql.local.nhost.run/v1": {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
documents: ["src/**/*.ts"],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
"./src/lib/graphql/__generated__/graphql.ts": {
|
||||
documents: ["src/lib/graphql/**/*.graphql"],
|
||||
plugins: [
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
"typescript-react-query",
|
||||
],
|
||||
config: {
|
||||
scalars: {
|
||||
UUID: "string",
|
||||
uuid: "string",
|
||||
timestamptz: "string",
|
||||
jsonb: "Record<string, any>",
|
||||
bigint: "number",
|
||||
bytea: "Buffer",
|
||||
citext: "string",
|
||||
},
|
||||
exposeQueryKeys: true,
|
||||
exposeFetcher: true,
|
||||
fetcher: {
|
||||
func: "../queryHooks#useAuthenticatedFetcher",
|
||||
isReactHook: true,
|
||||
},
|
||||
useTypeImports: true,
|
||||
reactQueryVersion: 5,
|
||||
},
|
||||
},
|
||||
"./schema.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
config: {
|
||||
includeDirectives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### 1. Create an Auth Provider
|
||||
|
||||
Create an authentication context to manage the user session:
|
||||
|
||||
```typescript
|
||||
// src/lib/nhost/AuthProvider.tsx
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { createClient, type NhostClient } from "@nhost/nhost-js";
|
||||
import { type Session } from "@nhost/nhost-js/auth";
|
||||
|
||||
interface AuthContextType {
|
||||
user: Session["user"] | null;
|
||||
session: Session | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
nhost: NhostClient;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<Session["user"] | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
|
||||
// Create the nhost client
|
||||
const nhost = useMemo(
|
||||
() =>
|
||||
createClient({
|
||||
region: import.meta.env.VITE_NHOST_REGION || "local",
|
||||
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
setIsLoading(false);
|
||||
|
||||
const unsubscribe = nhost.sessionStorage.onChange((currentSession) => {
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [nhost]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
nhost,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Create Query Provider
|
||||
|
||||
Set up React Query with the Nhost client:
|
||||
|
||||
```typescript
|
||||
// src/lib/graphql/QueryProvider.tsx
|
||||
import { type ReactNode } from "react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface QueryProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function QueryProvider({ children }: QueryProviderProps) {
|
||||
// Create the query client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 10 * 1000, // 10 seconds
|
||||
refetchOnWindowFocus: true,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Authenticated Fetcher Hook
|
||||
|
||||
Create a utility to make authenticated GraphQL requests with the Nhost client:
|
||||
|
||||
```typescript
|
||||
// src/lib/graphql/queryHooks.ts
|
||||
import { useCallback } from "react";
|
||||
import { useAuth } from "../nhost/AuthProvider";
|
||||
|
||||
// This wrapper returns a fetcher function that uses the authenticated nhost client
|
||||
export const useAuthenticatedFetcher = <TData, TVariables>(
|
||||
document: string | { query: string; variables?: TVariables },
|
||||
) => {
|
||||
const { nhost } = useAuth();
|
||||
|
||||
return useCallback(
|
||||
async (variables?: TVariables): Promise<TData> => {
|
||||
// Handle both string format or document object format
|
||||
const query = typeof document === "string" ? document : document.query;
|
||||
const documentVariables =
|
||||
typeof document === "object" ? document.variables : undefined;
|
||||
const mergedVariables = variables || documentVariables;
|
||||
|
||||
const resp = await nhost.graphql.request<TData>({
|
||||
query,
|
||||
variables: mergedVariables as Record<string, unknown>,
|
||||
});
|
||||
|
||||
if (!resp.body.data) {
|
||||
throw new Error(
|
||||
`Response does not contain data: ${JSON.stringify(resp.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return resp.body.data;
|
||||
},
|
||||
[nhost],
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Set Up Your App Providers
|
||||
|
||||
Wrap your application with the Auth and Query providers:
|
||||
|
||||
```tsx
|
||||
// src/main.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { AuthProvider } from "./lib/nhost/AuthProvider";
|
||||
import { QueryProvider } from "./lib/graphql/QueryProvider";
|
||||
|
||||
// Root component that sets up providers
|
||||
const Root = () => (
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<QueryProvider>
|
||||
<App />
|
||||
</QueryProvider>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) throw new Error("Root element not found");
|
||||
|
||||
createRoot(rootElement).render(<Root />);
|
||||
```
|
||||
|
||||
### 5. Define GraphQL Operations
|
||||
|
||||
Create a GraphQL file with your queries and mutations:
|
||||
|
||||
```graphql
|
||||
# src/lib/graphql/operations.graphql
|
||||
query GetNinjaTurtlesWithComments {
|
||||
ninjaTurtles {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
comments {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
email
|
||||
displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
|
||||
insert_comments_one(
|
||||
object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Generate TypeScript Types
|
||||
|
||||
Run the code generator:
|
||||
|
||||
```bash
|
||||
npx graphql-codegen
|
||||
```
|
||||
|
||||
You can also add a script to your package.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen --config codegen.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
npm run codegen
|
||||
# or
|
||||
yarn codegen
|
||||
# or
|
||||
pnpm codegen
|
||||
```
|
||||
|
||||
### 7. Use in Components
|
||||
|
||||
Finally, you can use the generated React Query hooks in your components:
|
||||
|
||||
```tsx
|
||||
// src/pages/Home.tsx
|
||||
import { type JSX } from "react";
|
||||
import {
|
||||
useGetNinjaTurtlesWithCommentsQuery,
|
||||
useAddCommentMutation,
|
||||
} from "../lib/graphql/__generated__/graphql";
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { isLoading } = useAuth();
|
||||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Query for data
|
||||
const {
|
||||
data,
|
||||
isLoading: loading,
|
||||
error,
|
||||
} = useGetNinjaTurtlesWithCommentsQuery();
|
||||
|
||||
// Mutation hook
|
||||
const { mutate: addComment } = useAddCommentMutation({
|
||||
onSuccess: () => {
|
||||
setCommentText("");
|
||||
setActiveCommentId(null);
|
||||
// Invalidate and refetch the ninja turtles query to get updated data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["GetNinjaTurtlesWithComments"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const handleAddComment = (turtleId: string) => {
|
||||
if (!commentText.trim()) return;
|
||||
|
||||
addComment({
|
||||
ninjaTurtleId: turtleId,
|
||||
comment: commentText,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) return <div>Loading ninja turtles...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Access the data
|
||||
const ninjaTurtles = data?.ninjaTurtles || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Ninja Turtles</h1>
|
||||
{ninjaTurtles.map((turtle) => (
|
||||
<div key={turtle.id}>
|
||||
<h2>{turtle.name}</h2>
|
||||
<p>{turtle.description}</p>
|
||||
|
||||
{/* Comments section */}
|
||||
<div>
|
||||
<h3>Comments ({turtle.comments.length})</h3>
|
||||
|
||||
{turtle.comments.map((comment) => (
|
||||
<div key={comment.id}>
|
||||
<p>{comment.comment}</p>
|
||||
<small>
|
||||
By{" "}
|
||||
{comment.user?.displayName ||
|
||||
comment.user?.email ||
|
||||
"Anonymous"}
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeCommentId === turtle.id ? (
|
||||
<div>
|
||||
<textarea
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Add your comment..."
|
||||
/>
|
||||
<div>
|
||||
<button onClick={() => setActiveCommentId(null)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={() => handleAddComment(turtle.id)}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setActiveCommentId(turtle.id)}>
|
||||
Add a comment
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Appendix: Known GraphQL CodeGen Issues
|
||||
|
||||
When using GraphQL Code Generator with both `useTypeImports: true` and a custom fetcher that's a React hook, there's a known bug ([issue #824](https://github.com/dotansimha/graphql-code-generator-community/issues/824)) that causes incorrect import statements in the generated code.
|
||||
|
||||
The problem occurs because when `useTypeImports` is enabled, the generator incorrectly adds the `type` keyword to the import statement for your custom fetcher function:
|
||||
|
||||
```ts
|
||||
import type { useAuthenticatedFetcher } from "../queryHooks";
|
||||
```
|
||||
|
||||
Since `useAuthenticatedFetcher` is a React hook that needs to be executed at runtime (not just used as a type), this causes TypeScript errors because the function can't be called when imported as a type.
|
||||
|
||||
To fix this issue, you need to modify the generated file to remove the `type` keyword from the import statement. This can be done with a post-processing wrapper script (`codegen-wrapper.sh`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# due to bug https://github.com/dotansimha/graphql-code-generator-community/issues/824
|
||||
|
||||
# Exit immediately if a command exits with a non-zero status.
|
||||
set -e
|
||||
|
||||
echo "Running GraphQL code generator..."
|
||||
# Run the original codegen command
|
||||
pnpm graphql-codegen --config codegen.ts
|
||||
|
||||
# Path to the generated file
|
||||
GENERATED_FILE="src/lib/graphql/__generated__/graphql.ts"
|
||||
|
||||
echo "Fixing import in $GENERATED_FILE..."
|
||||
if [ -f "$GENERATED_FILE" ]; then
|
||||
sed -i -e 's/import type { useAuthenticatedFetcher }/import { useAuthenticatedFetcher }/g' "$GENERATED_FILE"
|
||||
echo "Successfully removed \"type\" from useAuthenticatedFetcher import."
|
||||
else
|
||||
echo "Error: Generated file not found at $GENERATED_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All tasks completed successfully."
|
||||
```
|
||||
7
examples/guides/react-query/biome.json
Normal file
7
examples/guides/react-query/biome.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"linter": {
|
||||
"includes": ["**", "!src/lib/graphql/__generated__/graphql.ts"]
|
||||
}
|
||||
}
|
||||
31
examples/guides/react-query/codegen-wrapper.sh
Executable file
31
examples/guides/react-query/codegen-wrapper.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Running GraphQL code generator..."
|
||||
pnpm graphql-codegen --config codegen.ts
|
||||
|
||||
GENERATED_TS_FILE="src/lib/graphql/__generated__/graphql.ts"
|
||||
GENERATED_SCHEMA_FILE="schema.graphql"
|
||||
|
||||
if [ -f "$GENERATED_TS_FILE" ]; then
|
||||
echo "Fixing import in $GENERATED_TS_FILE..."
|
||||
# https://github.com/dotansimha/graphql-code-generator-community/issues/824
|
||||
sed -i -e 's/import type { useAuthenticatedFetcher }/import { useAuthenticatedFetcher }/g' "$GENERATED_TS_FILE"
|
||||
echo "Successfully removed \"type\" from useAuthenticatedFetcher import."
|
||||
echo "Formatting $GENERATED_TS_FILE..."
|
||||
biome check --write "$GENERATED_TS_FILE"
|
||||
else
|
||||
echo "Error: Generated TypeScript file not found at $GENERATED_TS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ -f "$GENERATED_SCHEMA_FILE" ]; then
|
||||
echo "Formatting $GENERATED_SCHEMA_FILE..."
|
||||
biome check --write "$GENERATED_SCHEMA_FILE"
|
||||
echo "Successfully formatted $GENERATED_SCHEMA_FILE"
|
||||
else
|
||||
echo "Warning: Generated schema file not found at $GENERATED_SCHEMA_FILE"
|
||||
fi
|
||||
|
||||
echo "All tasks completed successfully."
|
||||
52
examples/guides/react-query/codegen.ts
Normal file
52
examples/guides/react-query/codegen.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
"https://local.graphql.local.nhost.run/v1": {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
documents: ["src/**/*.ts"],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
"./src/lib/graphql/__generated__/graphql.ts": {
|
||||
documents: ["src/lib/graphql/**/*.graphql"],
|
||||
plugins: [
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
"typescript-react-query",
|
||||
],
|
||||
config: {
|
||||
scalars: {
|
||||
UUID: "string",
|
||||
uuid: "string",
|
||||
timestamptz: "string",
|
||||
jsonb: "Record<string, any>",
|
||||
bigint: "number",
|
||||
bytea: "Buffer",
|
||||
citext: "string",
|
||||
},
|
||||
exposeQueryKeys: true,
|
||||
exposeFetcher: true,
|
||||
fetcher: {
|
||||
func: "../queryHooks#useAuthenticatedFetcher",
|
||||
isReactHook: true,
|
||||
},
|
||||
useTypeImports: true,
|
||||
reactQueryVersion: 5,
|
||||
},
|
||||
},
|
||||
"./schema.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
config: {
|
||||
includeDirectives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
13
examples/guides/react-query/index.html
Normal file
13
examples/guides/react-query/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
examples/guides/react-query/package.json
Normal file
37
examples/guides/react-query/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "demos/react-query",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"generate": "bash codegen-wrapper.sh",
|
||||
"test": "pnpm test:typecheck && pnpm test:lint",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"test:lint": "biome check",
|
||||
"format": "biome format --write",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@nhost/nhost-js": "workspace:*",
|
||||
"@tanstack/react-query": "^5.79.0",
|
||||
"graphql": "^16.11.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.6",
|
||||
"@graphql-codegen/schema-ast": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^4.1.6",
|
||||
"@graphql-codegen/typescript-operations": "^4.6.1",
|
||||
"@graphql-codegen/typescript-react-query": "^6.1.1",
|
||||
"@tanstack/react-query-devtools": "^5.79.0",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1"
|
||||
}
|
||||
}
|
||||
4408
examples/guides/react-query/pnpm-lock.yaml
generated
Normal file
4408
examples/guides/react-query/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
examples/guides/react-query/public/vite.svg
Normal file
1
examples/guides/react-query/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
10143
examples/guides/react-query/schema.graphql
Normal file
10143
examples/guides/react-query/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
56
examples/guides/react-query/src/App.tsx
Normal file
56
examples/guides/react-query/src/App.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { JSX } from "react";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import Navigation from "./components/Navigation";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import Home from "./pages/Home";
|
||||
import Profile from "./pages/Profile";
|
||||
import SignIn from "./pages/SignIn";
|
||||
import SignUp from "./pages/SignUp";
|
||||
|
||||
// Root layout component to wrap all routes
|
||||
const RootLayout = (): JSX.Element => {
|
||||
return (
|
||||
<div className="flex-col min-h-screen">
|
||||
<Navigation />
|
||||
<main className="max-w-2xl mx-auto p-6 w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
<footer>
|
||||
<p
|
||||
className="text-sm text-center"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
© {new Date().getFullYear()} Nhost Demo
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Create router with routes
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route element={<RootLayout />}>
|
||||
<Route path="signin" element={<SignIn />} />
|
||||
<Route path="signup" element={<SignUp />} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="home" element={<Home />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Route>,
|
||||
),
|
||||
);
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
85
examples/guides/react-query/src/components/Navigation.tsx
Normal file
85
examples/guides/react-query/src/components/Navigation.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { JSX } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Navigation(): JSX.Element {
|
||||
const { isAuthenticated, nhost, session } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
// Helper function to determine if a link is active
|
||||
const isActive = (path: string): string => {
|
||||
return location.pathname === path ? "active" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-container">
|
||||
<div className="flex items-center">
|
||||
<span className="navbar-brand">Nhost Demo</span>
|
||||
<div className="navbar-links">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link to="/home" className={`nav-link ${isActive("/home")}`}>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
className={`nav-link ${isActive("/profile")}`}
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/signin"
|
||||
className={`nav-link ${isActive("/signin")}`}
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup"
|
||||
className={`nav-link ${isActive("/signup")}`}
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAuthenticated && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (session) {
|
||||
await nhost.auth.signOut({
|
||||
refreshToken: session.refreshToken,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="icon-button"
|
||||
title="Sign Out"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Sign Out"
|
||||
role="img"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({
|
||||
redirectTo = "/signin",
|
||||
}: ProtectedRouteProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to={redirectTo} />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
552
examples/guides/react-query/src/index.css
Normal file
552
examples/guides/react-query/src/index.css
Normal file
@@ -0,0 +1,552 @@
|
||||
/* Base styles */
|
||||
:root {
|
||||
--background: #030712;
|
||||
--foreground: #ffffff;
|
||||
--card-bg: #111827;
|
||||
--card-border: #1f2937;
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--secondary: #10b981;
|
||||
--secondary-hover: #059669;
|
||||
--accent: #8b5cf6;
|
||||
--accent-hover: #7c3aed;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-color: rgba(31, 41, 55, 0.7);
|
||||
--font-geist-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.space-y-5 > * + * {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.space-x-4 > * + * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, var(--primary), var(--accent));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
.glass-card {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.625rem 1rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: var(--secondary-hover);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: white;
|
||||
background-color: rgba(31, 41, 55, 0.7);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
border: 1px solid var(--border-color);
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid rgba(34, 197, 94, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(17, 24, 39, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
font-size: 1.125rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(31, 41, 55, 0.3);
|
||||
}
|
||||
|
||||
/* File upload styles */
|
||||
.file-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
border: 2px dashed rgba(99, 102, 241, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
background-color: rgba(31, 41, 55, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-upload:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 1.25rem 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Profile data */
|
||||
.profile-item {
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.profile-item strong {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
margin-right: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-link-danger {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-link-danger:hover {
|
||||
color: #f05252;
|
||||
}
|
||||
|
||||
/* Icon button */
|
||||
.icon-button {
|
||||
background-color: transparent;
|
||||
color: var(--primary);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.icon-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Table action icons */
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.action-icon:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-icon-view {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-icon-view:hover:not(:disabled) {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.action-icon-delete {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-icon-delete:hover:not(:disabled) {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #f05252;
|
||||
}
|
||||
|
||||
/* Tab styles */
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
background-color: rgba(31, 41, 55, 0.5);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tab-button:hover:not(.tab-active) {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-button.tab-active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.tab-button:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface QueryProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function QueryProvider({ children }: QueryProviderProps) {
|
||||
// Create the query client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 10 * 1000, // 10 seconds
|
||||
refetchOnWindowFocus: true,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
6944
examples/guides/react-query/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
6944
examples/guides/react-query/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
examples/guides/react-query/src/lib/graphql/queries.graphql
Normal file
28
examples/guides/react-query/src/lib/graphql/queries.graphql
Normal file
@@ -0,0 +1,28 @@
|
||||
query GetNinjaTurtlesWithComments {
|
||||
ninjaTurtles {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
comments {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
|
||||
insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
ninjaTurtleId
|
||||
}
|
||||
}
|
||||
33
examples/guides/react-query/src/lib/graphql/queryHooks.ts
Normal file
33
examples/guides/react-query/src/lib/graphql/queryHooks.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useCallback } from "react";
|
||||
import { useAuth } from "../nhost/AuthProvider";
|
||||
|
||||
// This wrapper returns a fetcher function that uses the authenticated nhost client
|
||||
export const useAuthenticatedFetcher = <TData, TVariables>(
|
||||
document: string | { query: string; variables?: TVariables },
|
||||
) => {
|
||||
const { nhost } = useAuth();
|
||||
|
||||
return useCallback(
|
||||
async (variables?: TVariables): Promise<TData> => {
|
||||
// Handle both string format or document object format
|
||||
const query = typeof document === "string" ? document : document.query;
|
||||
const documentVariables =
|
||||
typeof document === "object" ? document.variables : undefined;
|
||||
const mergedVariables = variables || documentVariables;
|
||||
|
||||
const resp = await nhost.graphql.request<TData>({
|
||||
query,
|
||||
variables: mergedVariables as Record<string, unknown>,
|
||||
});
|
||||
|
||||
if (!resp.body.data) {
|
||||
throw new Error(
|
||||
`Response does not contain data: ${JSON.stringify(resp.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return resp.body.data;
|
||||
},
|
||||
[nhost, document],
|
||||
);
|
||||
};
|
||||
175
examples/guides/react-query/src/lib/nhost/AuthProvider.tsx
Normal file
175
examples/guides/react-query/src/lib/nhost/AuthProvider.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { createClient, type NhostClient } from "@nhost/nhost-js";
|
||||
import type { Session } from "@nhost/nhost-js/auth";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* Authentication context interface providing access to user session state and Nhost client.
|
||||
* Used throughout the React application to access authentication-related data and operations.
|
||||
*/
|
||||
interface AuthContextType {
|
||||
/** Current authenticated user object, null if not authenticated */
|
||||
user: Session["user"] | null;
|
||||
/** Current session object containing tokens and user data, null if no active session */
|
||||
session: Session | null;
|
||||
/** Boolean indicating if user is currently authenticated */
|
||||
isAuthenticated: boolean;
|
||||
/** Boolean indicating if authentication state is still loading */
|
||||
isLoading: boolean;
|
||||
/** Nhost client instance for making authenticated requests */
|
||||
nhost: NhostClient;
|
||||
}
|
||||
|
||||
// Create React context for authentication state and nhost client
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthProvider component that provides authentication context to the React application.
|
||||
*
|
||||
* This component handles:
|
||||
* - Initializing the Nhost client with default EventEmitterStorage
|
||||
* - Managing authentication state (user, session, loading, authenticated status)
|
||||
* - Cross-tab session synchronization using sessionStorage.onChange events
|
||||
* - Page visibility and focus event handling to maintain session consistency
|
||||
* - Client-side only session management (no server-side rendering)
|
||||
*/
|
||||
export const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const [user, setUser] = useState<Session["user"] | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const lastRefreshTokenIdRef = useRef<string | null>(null);
|
||||
|
||||
// Initialize Nhost client with default SessionStorage (local storage)
|
||||
const nhost = useMemo(
|
||||
() =>
|
||||
createClient({
|
||||
region: import.meta.env.VITE_NHOST_REGION || "local",
|
||||
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles session reload when refresh token changes.
|
||||
* This detects when the session has been updated from other tabs.
|
||||
* Unlike the Next.js version, this only updates local state without server synchronization.
|
||||
*
|
||||
* @param currentRefreshTokenId - The current refresh token ID to compare against stored value
|
||||
*/
|
||||
const reloadSession = useCallback(
|
||||
(currentRefreshTokenId: string | null) => {
|
||||
if (currentRefreshTokenId !== lastRefreshTokenIdRef.current) {
|
||||
lastRefreshTokenIdRef.current = currentRefreshTokenId;
|
||||
|
||||
// Update local authentication state to match current session
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
}
|
||||
},
|
||||
[nhost],
|
||||
);
|
||||
|
||||
// Initialize authentication state and set up cross-tab session synchronization
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Load initial session state from Nhost client
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
lastRefreshTokenIdRef.current = currentSession?.refreshTokenId ?? null;
|
||||
setIsLoading(false);
|
||||
|
||||
// Subscribe to session changes from other browser tabs
|
||||
// This enables real-time synchronization when user signs in/out in another tab
|
||||
const unsubscribe = nhost.sessionStorage.onChange((session) => {
|
||||
reloadSession(session?.refreshTokenId ?? null);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [nhost, reloadSession]);
|
||||
|
||||
// Handle session changes from page focus events (for additional session consistency)
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Checks for session changes when page becomes visible or focused.
|
||||
* In the React SPA context, this provides additional consistency checks
|
||||
* though it's less critical than in the Next.js SSR version.
|
||||
*/
|
||||
const checkSessionOnFocus = () => {
|
||||
reloadSession(nhost.getUserSession()?.refreshTokenId ?? null);
|
||||
};
|
||||
|
||||
// Monitor page visibility changes (tab switching, window minimizing)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) {
|
||||
checkSessionOnFocus();
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor window focus events (clicking back into the browser window)
|
||||
window.addEventListener("focus", checkSessionOnFocus);
|
||||
|
||||
// Cleanup event listeners on component unmount
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", checkSessionOnFocus);
|
||||
window.removeEventListener("focus", checkSessionOnFocus);
|
||||
};
|
||||
}, [nhost, reloadSession]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
nhost,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to access the authentication context.
|
||||
*
|
||||
* Must be used within a component wrapped by AuthProvider.
|
||||
* Provides access to current user session, authentication state, and Nhost client.
|
||||
*
|
||||
* @throws {Error} When used outside of AuthProvider
|
||||
* @returns {AuthContextType} Authentication context containing user, session, and client
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { user, isAuthenticated, nhost } = useAuth();
|
||||
*
|
||||
* if (!isAuthenticated) {
|
||||
* return <div>Please sign in</div>;
|
||||
* }
|
||||
*
|
||||
* return <div>Welcome, {user?.displayName}!</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
22
examples/guides/react-query/src/main.tsx
Normal file
22
examples/guides/react-query/src/main.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { QueryProvider } from "./lib/graphql/QueryProvider";
|
||||
import { AuthProvider } from "./lib/nhost/AuthProvider";
|
||||
|
||||
// Root component that sets up providers
|
||||
const Root = () => (
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<QueryProvider>
|
||||
<App />
|
||||
</QueryProvider>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) throw new Error("Root element not found");
|
||||
|
||||
createRoot(rootElement).render(<Root />);
|
||||
217
examples/guides/react-query/src/pages/Home.css
Normal file
217
examples/guides/react-query/src/pages/Home.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/* Custom styles for Ninja Turtles tabs interface */
|
||||
.ninja-turtles-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ninja-turtles-title {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
color: #1a9c44;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Tab navigation */
|
||||
.turtle-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #1a9c44;
|
||||
margin-bottom: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.turtle-tab {
|
||||
padding: 10px 20px;
|
||||
margin-right: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.turtle-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(26, 156, 68, 0.1);
|
||||
}
|
||||
|
||||
.turtle-tab.active {
|
||||
color: white;
|
||||
background: #1a9c44;
|
||||
}
|
||||
|
||||
/* Turtle Card Styles */
|
||||
.turtle-card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.turtle-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.turtle-name {
|
||||
color: #1a9c44;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.turtle-description {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.turtle-date {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Comments section */
|
||||
.comments-section {
|
||||
margin-top: 25px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
color: #1a9c44;
|
||||
}
|
||||
|
||||
.comment-card {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(26, 156, 68, 0.05);
|
||||
border: 1px solid rgba(26, 156, 68, 0.1);
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #1a9c44;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Comment form */
|
||||
.comment-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comment-textarea {
|
||||
width: 100%;
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-textarea:focus {
|
||||
border-color: #1a9c44;
|
||||
box-shadow: 0 0 0 2px rgba(26, 156, 68, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #1a9c44;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #148035;
|
||||
}
|
||||
|
||||
.add-comment-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #1a9c44;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px 0;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.add-comment-button:hover {
|
||||
color: #148035;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.add-comment-button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.turtle-tabs {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.turtle-tab {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
204
examples/guides/react-query/src/pages/Home.tsx
Normal file
204
examples/guides/react-query/src/pages/Home.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type JSX, useState } from "react";
|
||||
import {
|
||||
useAddCommentMutation,
|
||||
useGetNinjaTurtlesWithCommentsQuery,
|
||||
} from "../lib/graphql/__generated__/graphql";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
import "./Home.css";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { isLoading } = useAuth();
|
||||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading: loading,
|
||||
error,
|
||||
} = useGetNinjaTurtlesWithCommentsQuery();
|
||||
|
||||
const { mutate: addComment } = useAddCommentMutation({
|
||||
onSuccess: () => {
|
||||
setCommentText("");
|
||||
setActiveCommentId(null);
|
||||
// Invalidate and refetch the ninja turtles query to get updated data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["GetNinjaTurtlesWithComments"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// If authentication is still loading, show a loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleAddComment = (turtleId: string) => {
|
||||
if (!commentText.trim()) return;
|
||||
|
||||
addComment({
|
||||
ninjaTurtleId: turtleId,
|
||||
comment: commentText,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading ninja turtles...</p>
|
||||
</div>
|
||||
);
|
||||
if (error)
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
Error loading ninja turtles: {(error as Error).message}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Access the data using the correct field name from the GraphQL response
|
||||
const ninjaTurtles = data?.ninjaTurtles || [];
|
||||
if (!ninjaTurtles || ninjaTurtles.length === 0) {
|
||||
return (
|
||||
<div className="no-turtles-container">
|
||||
<p>No ninja turtles found. Please add some!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Set the active tab to the first turtle if there's no active tab and there are turtles
|
||||
if (activeTabId === null) {
|
||||
setActiveTabId(ninjaTurtles[0] ? ninjaTurtles[0].id : null);
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ninja-turtles-container">
|
||||
<h1 className="ninja-turtles-title text-3xl font-bold mb-6">
|
||||
Teenage Mutant Ninja Turtles
|
||||
</h1>
|
||||
|
||||
{/* Tabs navigation */}
|
||||
<div className="turtle-tabs">
|
||||
{ninjaTurtles.map((turtle) => (
|
||||
<button
|
||||
key={turtle.id}
|
||||
type="button"
|
||||
className={`turtle-tab ${activeTabId === turtle.id ? "active" : ""}`}
|
||||
onClick={() => setActiveTabId(turtle.id)}
|
||||
>
|
||||
{turtle.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Display active turtle */}
|
||||
{ninjaTurtles
|
||||
.filter((turtle) => turtle.id === activeTabId)
|
||||
.map((turtle) => (
|
||||
<div key={turtle.id} className="turtle-card glass-card p-6">
|
||||
<div className="turtle-header">
|
||||
<h2 className="turtle-name text-2xl font-semibold">
|
||||
{turtle.name}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="turtle-description">{turtle.description}</p>
|
||||
|
||||
<div className="turtle-date">
|
||||
Added on {formatDate(turtle.createdAt || turtle.createdAt)}
|
||||
</div>
|
||||
|
||||
<div className="comments-section">
|
||||
<h3 className="comments-title">
|
||||
Comments ({turtle.comments.length})
|
||||
</h3>
|
||||
|
||||
{turtle.comments.map((comment) => (
|
||||
<div key={comment.id} className="comment-card">
|
||||
<p className="comment-text">{comment.comment}</p>
|
||||
<div className="comment-meta">
|
||||
<div className="comment-avatar">
|
||||
{(comment.user?.displayName || comment.user?.email || "?")
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<p>
|
||||
{comment.user?.displayName ||
|
||||
comment.user?.email ||
|
||||
"Anonymous"}{" "}
|
||||
- {formatDate(comment.createdAt || comment.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeCommentId === turtle.id ? (
|
||||
<div className="comment-form">
|
||||
<textarea
|
||||
className="comment-textarea"
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Add your comment..."
|
||||
rows={3}
|
||||
/>
|
||||
<div className="comment-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveCommentId(null);
|
||||
setCommentText("");
|
||||
}}
|
||||
className="btn cancel-button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddComment(turtle.id)}
|
||||
className="btn submit-button"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveCommentId(turtle.id)}
|
||||
className="add-comment-button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Add Comment"
|
||||
role="img"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
Add a comment
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
examples/guides/react-query/src/pages/Profile.tsx
Normal file
66
examples/guides/react-query/src/pages/Profile.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { JSX } from "react";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Profile(): JSX.Element {
|
||||
const { user, session } = useAuth();
|
||||
|
||||
// ProtectedRoute component now handles authentication check
|
||||
// We can just focus on the component logic here
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Your Profile</h1>
|
||||
|
||||
<div className="glass-card p-8 mb-6">
|
||||
<div className="space-y-5">
|
||||
<div className="profile-item">
|
||||
<strong>Display Name:</strong>
|
||||
<span className="ml-2">{user?.displayName || "Not set"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Email:</strong>
|
||||
<span className="ml-2">{user?.email || "Not available"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>User ID:</strong>
|
||||
<span
|
||||
className="ml-2"
|
||||
style={{
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{user?.id || "Not available"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Roles:</strong>
|
||||
<span className="ml-2">{user?.roles?.join(", ") || "None"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Email Verified:</strong>
|
||||
<span className="ml-2">{user?.emailVerified ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-8 mb-6">
|
||||
<h3 className="text-xl mb-4">Session Information</h3>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
refreshTokenId: session?.refreshTokenId,
|
||||
accessTokenExpiresIn: session?.accessTokenExpiresIn,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
examples/guides/react-query/src/pages/SignIn.tsx
Normal file
120
examples/guides/react-query/src/pages/SignIn.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { ErrorResponse } from "@nhost/nhost-js/auth";
|
||||
import type { FetchError } from "@nhost/nhost-js/fetch";
|
||||
import { type JSX, useEffect, useId, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function SignIn(): JSX.Element {
|
||||
const { nhost, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(
|
||||
params.get("error") || null,
|
||||
);
|
||||
|
||||
const isVerifying = params.has("fromVerify");
|
||||
|
||||
// Use useEffect for navigation after authentication is confirmed
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !isVerifying) {
|
||||
navigate("/home");
|
||||
}
|
||||
}, [isAuthenticated, isVerifying, navigate]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Use the signIn function from auth context
|
||||
const response = await nhost.auth.signInEmailPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// Check if MFA is required
|
||||
if (response.body?.mfa) {
|
||||
navigate(`/signin/mfa?ticket=${response.body.mfa.ticket}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a session, sign in was successful
|
||||
if (response.body?.session) {
|
||||
navigate("/home");
|
||||
} else {
|
||||
setError("Failed to sign in");
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as FetchError<ErrorResponse>;
|
||||
setError(`An error occurred during sign in: ${error.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
|
||||
|
||||
<div className="glass-card w-full p-8 mb-6">
|
||||
<h2 className="text-2xl mb-6">Sign In</h2>
|
||||
<div>
|
||||
<div className="tabs-container">
|
||||
<button type="button" className="tab-button tab-active">
|
||||
Email + Password
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor={emailId}>Email</label>
|
||||
<input
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={passwordId}>Password</label>
|
||||
<input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing In..." : "Sign In"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p>
|
||||
Don't have an account? <Link to="/signup">Sign Up</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
examples/guides/react-query/src/pages/SignUp.tsx
Normal file
127
examples/guides/react-query/src/pages/SignUp.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { ErrorResponse } from "@nhost/nhost-js/auth";
|
||||
import type { FetchError } from "@nhost/nhost-js/fetch";
|
||||
import { type JSX, useId, useState } from "react";
|
||||
import { Link, Navigate, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function SignUp(): JSX.Element {
|
||||
const { nhost, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const displayNameId = useId();
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [displayName, setDisplayName] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// If already authenticated, redirect to profile
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/home" />;
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await nhost.auth.signUpEmailPassword({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
displayName,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body) {
|
||||
// Successfully signed up and automatically signed in
|
||||
navigate("/home");
|
||||
} else {
|
||||
// Verification email sent
|
||||
navigate("/verify");
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as FetchError<ErrorResponse>;
|
||||
setError(`An error occurred during sign up: ${error.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
|
||||
|
||||
<div className="glass-card w-full p-8 mb-6">
|
||||
<h2 className="text-2xl mb-6">Sign Up</h2>
|
||||
|
||||
<div>
|
||||
<div className="tabs-container">
|
||||
<button type="button" className="tab-button tab-active">
|
||||
Email + Password
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor={displayNameId}>Display Name</label>
|
||||
<input
|
||||
id={displayNameId}
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={emailId}>Email</label>
|
||||
<input
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={passwordId}>Password</label>
|
||||
<input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs mt-1 text-gray-400">
|
||||
Password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing Up..." : "Sign Up"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p>
|
||||
Already have an account? <Link to="/signin">Sign In</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
examples/guides/react-query/src/vite-env.d.ts
vendored
Normal file
11
examples/guides/react-query/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_NHOST_REGION: string | undefined;
|
||||
readonly VITE_NHOST_SUBDOMAIN: string | undefined;
|
||||
readonly VITE_ENV: string | undefined;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
6
examples/guides/react-query/tsconfig.json
Normal file
6
examples/guides/react-query/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/frontend.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
4
examples/guides/react-query/tsconfig.node.json
Normal file
4
examples/guides/react-query/tsconfig.node.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/vite.json"
|
||||
}
|
||||
7
examples/guides/react-query/vite.config.ts
Normal file
7
examples/guides/react-query/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
47
flake.nix
47
flake.nix
@@ -22,46 +22,6 @@
|
||||
nix2containerPkgs = nix2container.packages.${system};
|
||||
nixops-lib = lib { inherit pkgs nix2containerPkgs; };
|
||||
|
||||
node_modules = nixops-lib.js.mkNodeModules {
|
||||
name = "node-modules";
|
||||
version = "0.0.0-dev";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ./.;
|
||||
include = [
|
||||
./.npmrc
|
||||
./pnpm-workspace.yaml
|
||||
|
||||
# find . -name package.json | grep -v node_modules | grep -v deprecated
|
||||
./package.json
|
||||
./docs/package.json
|
||||
./dashboard/package.json
|
||||
./examples/demos/react-demo/package.json
|
||||
./examples/demos/ReactNativeDemo/package.json
|
||||
./examples/demos/express/package.json
|
||||
./examples/demos/vue-demo/package.json
|
||||
./examples/demos/sveltekit-demo/package.json
|
||||
./examples/demos/package.json
|
||||
./examples/demos/nextjs-ssr-demo/package.json
|
||||
./packages/nhost-js/package.json
|
||||
|
||||
# find . -name pnpm-lock.yaml | grep -v node_modules | grep -v deprecated
|
||||
./pnpm-lock.yaml
|
||||
./docs/pnpm-lock.yaml
|
||||
./dashboard/pnpm-lock.yaml
|
||||
./examples/docker-compose/functions/pnpm-lock.yaml
|
||||
./examples/demos/react-demo/pnpm-lock.yaml
|
||||
./examples/demos/ReactNativeDemo/pnpm-lock.yaml
|
||||
./examples/demos/pnpm-lock.yaml
|
||||
./examples/demos/express/pnpm-lock.yaml
|
||||
./examples/demos/vue-demo/pnpm-lock.yaml
|
||||
./examples/demos/sveltekit-demo/pnpm-lock.yaml
|
||||
./examples/demos/nextjs-ssr-demo/pnpm-lock.yaml
|
||||
./packages/nhost-js/pnpm-lock.yaml
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
codegenf = import ./tools/codegen/project.nix {
|
||||
inherit self pkgs nix-filter nixops-lib;
|
||||
};
|
||||
@@ -78,6 +38,10 @@
|
||||
inherit self pkgs nix-filter nixops-lib;
|
||||
};
|
||||
|
||||
guidesf = import ./examples/guides/project.nix {
|
||||
inherit self pkgs nix-filter nixops-lib nix2containerPkgs;
|
||||
};
|
||||
|
||||
mintlify-openapif = import ./tools/mintlify-openapi/project.nix {
|
||||
inherit self pkgs nix-filter nixops-lib;
|
||||
};
|
||||
@@ -99,6 +63,7 @@
|
||||
codegen = codegenf.check;
|
||||
dashboard = dashboardf.check;
|
||||
demos = demosf.check;
|
||||
guides = guidesf.check;
|
||||
docs = docsf.check;
|
||||
mintlify-openapi = mintlify-openapif.check;
|
||||
nhost-js = nhost-jsf.check;
|
||||
@@ -146,6 +111,7 @@
|
||||
codegen = codegenf.devShell;
|
||||
dashboard = dashboardf.devShell;
|
||||
demos = demosf.devShell;
|
||||
guides = guidesf.devShell;
|
||||
docs = docsf.devShell;
|
||||
mintlify-openapi = mintlify-openapif.devShell;
|
||||
nhost-js = nhost-jsf.devShell;
|
||||
@@ -157,6 +123,7 @@
|
||||
dashboard = dashboardf.package;
|
||||
dashboard-docker-image = dashboardf.dockerImage;
|
||||
demos = demosf.package;
|
||||
guides = guidesf.package;
|
||||
mintlify-openapi = mintlify-openapif.package;
|
||||
nhost-js = nhost-jsf.package;
|
||||
nixops = nixopsf.package;
|
||||
|
||||
@@ -18,6 +18,8 @@ let
|
||||
pkgs.stdenv.mkDerivation {
|
||||
inherit name version src;
|
||||
|
||||
dontFixup = true;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pnpm_10
|
||||
cacert
|
||||
@@ -38,7 +40,7 @@ let
|
||||
|
||||
for absdir in $(pnpm list --recursive --depth=-1 --parseable); do
|
||||
dir=$(realpath --relative-to="$PWD" "$absdir")
|
||||
echo "➜ Copying node_modules for $dir"
|
||||
echo " ➜ Copying node_modules for $dir"
|
||||
mkdir -p $out/$dir
|
||||
cp -r $dir/node_modules $out/$dir/node_modules
|
||||
done
|
||||
@@ -100,6 +102,7 @@ let
|
||||
|
||||
for absdir in $(pnpm list --recursive --depth=-1 --parseable); do
|
||||
dir=$(realpath --relative-to="$PWD" "$absdir")
|
||||
echo " ➜ Copying node_modules for $dir"
|
||||
cp -r ${node_modules}/$dir/node_modules $dir/node_modules
|
||||
done
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ packages:
|
||||
- docs
|
||||
- packages/**
|
||||
- examples/demos/**
|
||||
- examples/guides/**
|
||||
- '!**/test/**'
|
||||
- '!out/**'
|
||||
- '!**/functions'
|
||||
|
||||
Reference in New Issue
Block a user