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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-f0211d31ead8e27f200356bf615f34eee1e96ec6b068039a36b1506d32c35692">+53/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>queryHooks.ts</strong><dd><code>Create React Query
authenticated fetcher</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-45c18c55c9756b413c5d4b9a71bd04a53d78706eb9a2a611f2fd0a1ab7ce6b51">+33/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>vite.config.ts</strong><dd><code>Add Vite config for React
Apollo example</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-146d3d4bcf230a225998f3c68de6ffa9f19e16b85bb5ca882608d76e7b086566">+7/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>vite.config.ts</strong><dd><code>Add Vite config for React
Query example</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-22a14bf686832a47057d73f552546a1d56a1f34cd34666acf741940feca0605f">+7/-0</a>&nbsp;
&nbsp; &nbsp; </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>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-71a439d2114fc41855bdb2041134bbc4de64f886f06f3f50ed2fd31cc61c84a5">+175/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>AuthProvider.tsx</strong><dd><code>Implement Nhost
AuthProvider for React Query</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-34697170eae721899cacb8faca334b60793b405b3f2ee327fecd4d6944338ed1">+175/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>Home.tsx</strong><dd><code>Add Home page with Apollo
queries</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-407aac78333f6d47a86500b3e0c2311f1967ce50ba1a7ef4f022b8a50c013160">+203/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>Home.tsx</strong><dd><code>Add Home page with React Query
hooks</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-c91862e4a735090baf145ed64f347a460446a25b8d5061b6b928b88ddb146008">+204/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>SignUp.tsx</strong><dd><code>Add SignUp page and
form</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-9a841d153068ba2ba45e3b00c3979eca2048037670945ff079bce6645780f447">+127/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>SignUp.tsx</strong><dd><code>Add SignUp page and
form</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-53b2663ba20acd8379c5dbbb671dd0c1badbb24955c7b9b7d9115843bd9ea859">+127/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>SignIn.tsx</strong><dd><code>Add SignIn page and
form</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-fd0d7f5b422c5c3ec70fdeb95da10479b90132a571de56307d9b44fc69aaf27b">+120/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>SignIn.tsx</strong><dd><code>Add SignIn page and
form</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-ec047013e51fbbbc17b641287e763558c5182325be0fe86d44e439eeca6e9ad1">+120/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>Profile.tsx</strong><dd><code>Add Profile page
display</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-53e4cf70b4b84819e216a602241d5379b9402a2412af122b5a870ec0bc8cd6a1">+66/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>Profile.tsx</strong><dd><code>Add Profile page
display</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-13fbb13999bb0001387c3d9a5015974ffece9575c1e6240a21157a07090b3374">+66/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>Navigation.tsx</strong><dd><code>Add navigation bar
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-eb031d8883524edeab44c5b665a2429cffb1bac75d8f0dc1dc7bc3c42aa9c068">+85/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>Navigation.tsx</strong><dd><code>Add navigation bar
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-3d34860dbf6645315068dd62f3be52745294be2729e4dc718e40ad0125669204">+85/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>ProtectedRoute.tsx</strong><dd><code>Add protected route
wrapper</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-55c80a4fab944fa1c3210cce0fd0092776a01046ee9893c88c69d0c5f5f5458b">+26/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>ProtectedRoute.tsx</strong><dd><code>Add protected route
wrapper</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-bfc4913eb77b8cbe8115f7833bfdf1aad595496f3160dbf43eabf829f0d94f5a">+26/-0</a>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>Makefile</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3460/files#diff-85a3083c78e211e9eb36d741342bcbc85a1a0c375060f45c5426b560196de27f">+17/-0</a>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
</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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; </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>&nbsp;
&nbsp; &nbsp; </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>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr></tr></tbody></table>

</details>

___
This commit is contained in:
David Barroso
2025-09-05 14:59:11 +02:00
parent 3a41251caf
commit be6af4f157
71 changed files with 48521 additions and 285 deletions

View 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
View File

@@ -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

View File

@@ -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
View File

@@ -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:

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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"

View 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
View 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
View 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
View 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

View 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>
);
}
```

View File

@@ -0,0 +1,7 @@
{
"root": false,
"extends": "//",
"linter": {
"includes": ["**", "!src/lib/graphql/__generated__/graphql.ts"]
}
}

View 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."

View 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;

View 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>

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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;

View 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>
);
}

View File

@@ -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 />;
}

View 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;
}

File diff suppressed because it is too large Load Diff

View 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]);
};

View 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
}
}

View 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;
};

View 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 />);

View 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;
}
}

View 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>
);
}

View 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>
);
}

View 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&apos;t have an account? <Link to="/signup">Sign Up</Link>
</p>
</div>
</div>
);
}

View 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>
);
}

View 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;
}

View 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" }]
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../build/configs/tsconfig/vite.json"
}

View 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
View 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

View 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."
```

View File

@@ -0,0 +1,7 @@
{
"root": false,
"extends": "//",
"linter": {
"includes": ["**", "!src/lib/graphql/__generated__/graphql.ts"]
}
}

View 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."

View 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;

View 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>

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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;

View 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>
);
}

View File

@@ -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 />;
}

View 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;
}

View File

@@ -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>
);
}

File diff suppressed because it is too large Load Diff

View 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
}
}

View 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],
);
};

View 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;
};

View 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 />);

View 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;
}
}

View 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>
);
}

View 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>
);
}

View 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&apos;t have an account? <Link to="/signup">Sign Up</Link>
</p>
</div>
</div>
);
}

View 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>
);
}

View 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;
}

View 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" }]
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../build/configs/tsconfig/vite.json"
}

View File

@@ -0,0 +1,7 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});

View File

@@ -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;

View File

@@ -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

View File

@@ -3,6 +3,7 @@ packages:
- docs
- packages/**
- examples/demos/**
- examples/guides/**
- '!**/test/**'
- '!out/**'
- '!**/functions'