feat: Infinite query hook block (#34650)
* infinite list * infinite list block * registration * add missing supportedFrameworks * Add tables to the supabase project. Generate the types for it. * Refactor the infinite list query to be just a hook. * Clean up the block. Add comments. * Regenerate the registry. * Fix the docs, the block is not framework-dependent. * Set the package versions to * to be defined by other packages. * Minor fixes to the block. * Fix the examples. * Fix the docs for the new hook. * Fix the demo. * Add more migrations to the db. * Fix various issues with the query. Rewrote it to useSyncExternalStore. * Fix the SSR for the hook. * More fixes. * Try initializing the store in a useEffect. * Fix the pnpm-lock file. * Minor fixes in the docs. * Put the infinite list under a reusable components section. * Update apps/ui-library/registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts * Change the example DB to use todos. * Update the docs to be about Todos quickstart. * List edits * Fix link * Regenerate the registry. * Add query hook to the landing page. --------- Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com> Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
@@ -34,3 +34,11 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
||||
## Supabase types
|
||||
|
||||
To regenerate the Supabase database types, run
|
||||
|
||||
```
|
||||
supabase gen types --local > registry/default/fixtures/database.types.ts
|
||||
```
|
||||
|
||||
@@ -343,6 +343,18 @@ export const Index: Record<string, any> = {
|
||||
chunks: []
|
||||
}
|
||||
,
|
||||
"infinite-query-hook": {
|
||||
name: "infinite-query-hook",
|
||||
type: "registry:block",
|
||||
registryDependencies: [],
|
||||
|
||||
source: "",
|
||||
files: ["registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
}
|
||||
,
|
||||
"supabase-client-nextjs": {
|
||||
name: "supabase-client-nextjs",
|
||||
type: "registry:lib",
|
||||
@@ -486,6 +498,18 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
}
|
||||
,
|
||||
"infinite-query-hook-demo": {
|
||||
name: "infinite-query-hook-demo",
|
||||
type: "registry:example",
|
||||
registryDependencies: [],
|
||||
component: React.lazy(() => import("@/registry/default/examples/infinite-query-hook-demo.tsx")),
|
||||
source: "",
|
||||
files: ["registry/default/examples/infinite-query-hook-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -178,6 +178,24 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalGridLine />
|
||||
|
||||
{/* Infinite Query Hook */}
|
||||
<div className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 pt-16 pb-6 text-xs uppercase font-mono text-foreground-light tracking-wider relative flex justify-between items-center">
|
||||
<span>Infinite Query Hook</span>
|
||||
<Link
|
||||
className="text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2"
|
||||
href="/docs/nextjs/social-auth"
|
||||
>
|
||||
Go to block ➔
|
||||
</Link>
|
||||
</div>
|
||||
<HorizontalGridLine />
|
||||
<div className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 relative">
|
||||
<div className="-mt-4">
|
||||
<BlockPreview name="infinite-list-demo" />
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalGridLine />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
18
apps/ui-library/app/example/infinite-list-demo/layout.tsx
Normal file
18
apps/ui-library/app/example/infinite-list-demo/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { BaseInjector } from './../base-injector'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Infinite Data Table Demo',
|
||||
description: 'Demonstration of the Infinite Data Table component.',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<BaseInjector />
|
||||
<div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
5
apps/ui-library/app/example/infinite-list-demo/page.tsx
Normal file
5
apps/ui-library/app/example/infinite-list-demo/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import InfiniteQueryHookDemo from '@/registry/default/examples/infinite-query-hook-demo'
|
||||
|
||||
export default function Page() {
|
||||
return <InfiniteQueryHookDemo />
|
||||
}
|
||||
@@ -97,6 +97,14 @@ export const componentPages: SidebarNavGroup = {
|
||||
items: [],
|
||||
commandItemLabel: 'Realtime Chat',
|
||||
},
|
||||
{
|
||||
title: 'Infinite Query Hook',
|
||||
supportedFrameworks: [],
|
||||
href: '/docs/infinite-query-hook',
|
||||
new: true,
|
||||
items: [],
|
||||
commandItemLabel: 'Infinite Query Hook',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
290
apps/ui-library/content/docs/infinite-query-hook.mdx
Normal file
290
apps/ui-library/content/docs/infinite-query-hook.mdx
Normal file
@@ -0,0 +1,290 @@
|
||||
---
|
||||
title: Infinite Query Hook
|
||||
description: React hook for infinite lists, fetching data from Supabase.
|
||||
---
|
||||
|
||||
<BlockPreview name="infinite-list-demo" />
|
||||
|
||||
## Installation
|
||||
|
||||
<BlockItem
|
||||
name="infinite-query-hook"
|
||||
description="Installs the Infinite List component and necessary Supabase client setup."
|
||||
/>
|
||||
|
||||
## Folder structure
|
||||
|
||||
<RegistryBlock itemName="infinite-query-hook" />
|
||||
|
||||
## Introduction
|
||||
|
||||
The Infinite Query Hook provides a single React hook which will make it easier to load data progressively from your Supabase database. It handles data fetching and pagination state, It is meant to be used with infinite lists or tables.
|
||||
The hook is fully typed, provided you have generated and setup your database types.
|
||||
|
||||
## Adding types
|
||||
|
||||
Before using this hook, we **highly** recommend you setup database types in your project. This will make the hook fully-typesafe. More info about generating Typescript types from database schema [here](https://supabase.com/docs/guides/api/rest/generating-types)
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `tableName` | `string` | **Required.** The name of the Supabase table to fetch data from. |
|
||||
| `columns` | `string` | Columns to select from the table. Defaults to `'*'`. |
|
||||
| `pageSize` | `number` | Number of items to fetch per page. Defaults to `20`. |
|
||||
| `trailingQuery` | `(query: SupabaseSelectBuilder) => SupabaseSelectBuilder` | Function to apply filters or sorting to the Supabase query. |
|
||||
|
||||
## Return type
|
||||
|
||||
data, count, isSuccess, isLoading, isFetching, error, hasMore, fetchNextPage
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------- | ------------- | ----------------------------------------------------------------------------------- |
|
||||
| `data` | `TableData[]` | An array of fetched items. |
|
||||
| `count` | `number` | Number of total items in the database. It takes `trailingQuery` into consideration. |
|
||||
| `isSuccess` | `boolean` | It's true if the last API call succeeded. |
|
||||
| `isLoading` | `boolean` | It's true only for the initial fetch. |
|
||||
| `isFetching` | `boolean` | It's true for the initial and all incremental fetches. |
|
||||
| `error` | `any` | The error from the last fetch. |
|
||||
| `hasMore` | `boolean` | Whether the query has finished fetching all items from the database |
|
||||
| `fetchNextPage` | `() => void` | Sends a new request for the next items |
|
||||
|
||||
## Type safety
|
||||
|
||||
The hook will use the typed defined on your Supabase client if they're setup ([more info](https://supabase.com/docs/reference/javascript/typescript-support)).
|
||||
|
||||
The hook also supports an custom defined result type by using `useInfiniteQuery<T>`. For example, if you have a custom type for `Product`, you can use it like this `useInfiniteQuery<Product>`.
|
||||
|
||||
## Usage
|
||||
|
||||
### With sorting
|
||||
|
||||
```tsx
|
||||
const { data, fetchNextPage } = useInfiniteQuery({
|
||||
tableName: 'products',
|
||||
columns: '*',
|
||||
pageSize: 10,
|
||||
trailingQuery: (query) => query.order('created_at', { ascending: false }),
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.map((item) => (
|
||||
<ProductCard key={item.id} product={item} />
|
||||
))}
|
||||
<Button onClick={fetchNextPage}>Load more products</Button>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
### With filtering on search params
|
||||
|
||||
This example will filter based on a search param like `example.com/?q=hello`.
|
||||
|
||||
```tsx
|
||||
const params = useSearchParams()
|
||||
const searchQuery = params.get('q')
|
||||
|
||||
const { data, isLoading, isFetching, fetchNextPage, count, isSuccess } = useInfiniteQuery({
|
||||
tableName: 'products',
|
||||
columns: '*',
|
||||
pageSize: 10,
|
||||
trailingQuery: (query) => {
|
||||
if (searchQuery && searchQuery.length > 0) {
|
||||
query = query.ilike('name', `%${searchQuery}%`)
|
||||
}
|
||||
return query
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.map((item) => (
|
||||
<ProductCard key={item.id} product={item} />
|
||||
))}
|
||||
<Button onClick={fetchNextPage}>Load more products</Button>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
## Reusable components
|
||||
|
||||
### Infinite list (fetches as you scroll)
|
||||
|
||||
The following component abstracts the hook into a component. It includes few utility components for no results and end of the list.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
SupabaseQueryHandler,
|
||||
SupabaseTableData,
|
||||
SupabaseTableName,
|
||||
useInfiniteQuery,
|
||||
} from '@/hooks/use-infinite-query'
|
||||
import * as React from 'react'
|
||||
|
||||
interface InfiniteListProps<TableName extends SupabaseTableName> {
|
||||
tableName: TableName
|
||||
columns?: string
|
||||
pageSize?: number
|
||||
trailingQuery?: SupabaseQueryHandler<TableName>
|
||||
renderItem: (item: SupabaseTableData<TableName>, index: number) => React.ReactNode
|
||||
className?: string
|
||||
renderNoResults?: () => React.ReactNode
|
||||
renderEndMessage?: () => React.ReactNode
|
||||
renderSkeleton?: (count: number) => React.ReactNode
|
||||
}
|
||||
|
||||
const DefaultNoResults = () => (
|
||||
<div className="text-center text-muted-foreground py-10">No results.</div>
|
||||
)
|
||||
|
||||
const DefaultEndMessage = () => (
|
||||
<div className="text-center text-muted-foreground py-4 text-sm">You've reached the end.</div>
|
||||
)
|
||||
|
||||
const defaultSkeleton = (count: number) => (
|
||||
<div className="flex flex-col gap-2 px-4">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div key={index} className="h-4 w-full bg-muted animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
export function InfiniteList<TableName extends SupabaseTableName>({
|
||||
tableName,
|
||||
columns = '*',
|
||||
pageSize = 20,
|
||||
trailingQuery,
|
||||
renderItem,
|
||||
className,
|
||||
renderNoResults = DefaultNoResults,
|
||||
renderEndMessage = DefaultEndMessage,
|
||||
renderSkeleton = defaultSkeleton,
|
||||
}: InfiniteListProps<TableName>) {
|
||||
const { data, isFetching, hasMore, fetchNextPage, isSuccess } = useInfiniteQuery({
|
||||
tableName,
|
||||
columns,
|
||||
pageSize,
|
||||
trailingQuery,
|
||||
})
|
||||
|
||||
// Ref for the scrolling container
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Intersection observer logic - target the last rendered *item* or a dedicated sentinel
|
||||
const loadMoreSentinelRef = React.useRef<HTMLDivElement>(null)
|
||||
const observer = React.useRef<IntersectionObserver | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (observer.current) observer.current.disconnect()
|
||||
|
||||
observer.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore && !isFetching) {
|
||||
fetchNextPage()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainerRef.current, // Use the scroll container for scroll detection
|
||||
threshold: 0.1, // Trigger when 10% of the target is visible
|
||||
rootMargin: '0px 0px 100px 0px', // Trigger loading a bit before reaching the end
|
||||
}
|
||||
)
|
||||
|
||||
if (loadMoreSentinelRef.current) {
|
||||
observer.current.observe(loadMoreSentinelRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observer.current) observer.current.disconnect()
|
||||
}
|
||||
}, [isFetching, hasMore, fetchNextPage])
|
||||
|
||||
return (
|
||||
<div ref={scrollContainerRef} className={cn('relative h-full overflow-auto', className)}>
|
||||
<div>
|
||||
{isSuccess && data.length === 0 && renderNoResults()}
|
||||
|
||||
{data.map((item, index) => renderItem(item, index))}
|
||||
|
||||
{isFetching && renderSkeleton && renderSkeleton(pageSize)}
|
||||
|
||||
<div ref={loadMoreSentinelRef} style={{ height: '1px' }} />
|
||||
|
||||
{!hasMore && data.length > 0 && renderEndMessage()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Use the `InfiniteList` component with the [Todo List](https://supabase.com/dashboard/project/_/sql/quickstarts) quickstart.
|
||||
|
||||
Add `<InfiniteListDemo />` to a page to see it in action.
|
||||
Ensure the [Checkbox](https://ui.shadcn.com/docs/components/checkbox) component from shadcn/ui is installed, and [regenerate/download](https://supabase.com/docs/guides/api/rest/generating-types) types after running the quickstart.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { InfiniteList } from './infinite-component'
|
||||
import { SupabaseQueryHandler } from '@/hooks/use-infinite-query'
|
||||
import { Database } from '@/lib/supabase.types'
|
||||
|
||||
type TodoTask = Database['public']['Tables']['todos']['Row']
|
||||
|
||||
// Define how each item should be rendered
|
||||
const renderTodoItem = (todo: TodoTask) => {
|
||||
return (
|
||||
<div
|
||||
key={todo.id}
|
||||
className="border-b py-3 px-4 hover:bg-muted flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox defaultChecked={todo.is_complete ?? false} />
|
||||
<div>
|
||||
<span className="font-medium text-sm text-foreground">{todo.task}</span>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{new Date(todo.inserted_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const orderByInsertedAt: SupabaseQueryHandler<'todos'> = (query) => {
|
||||
return query.order('inserted_at', { ascending: false })
|
||||
}
|
||||
|
||||
export const InfiniteListDemo = () => {
|
||||
return (
|
||||
<div className="bg-background h-[600px]">
|
||||
<InfiniteList
|
||||
tableName="todos"
|
||||
renderItem={renderTodoItem}
|
||||
pageSize={3}
|
||||
trailingQuery={orderByInsertedAt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Callout>
|
||||
The Todo List table has Row Level Security (RLS) enabled by default. Feel free disable it
|
||||
temporarily while testing. With RLS enabled, you will get an [empty
|
||||
array](https://supabase.com/docs/guides/troubleshooting/why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx)
|
||||
of results by default. [Read
|
||||
more](https://supabase.com/docs/guides/database/postgres/row-level-security) about RLS.
|
||||
</Callout>
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Generating Typescript types from the database](https://supabase.com/docs/reference/javascript/typescript-support)
|
||||
- [Supabase Database API](https://supabase.com/docs/reference/javascript/select)
|
||||
- [Supabase Pagination](https://supabase.com/docs/reference/javascript/select#pagination)
|
||||
- [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
|
||||
@@ -46,6 +46,7 @@ export function generateRegistryTree(registryPath: string): RegistryNode[] {
|
||||
// Remove any paths in the file content that point to the block directory.
|
||||
const content = file.content
|
||||
.replaceAll(/@\/registry\/default\/blocks\/.+?\//gi, '@/')
|
||||
.replaceAll(/@\/registry\/default\/fixtures\//gi, '@/')
|
||||
.replaceAll(/@\/registry\/default\//gi, '@/')
|
||||
.replaceAll(/@\/clients\/.+?\//gi, '@/')
|
||||
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@radix-ui/react-avatar": "*",
|
||||
"@radix-ui/react-checkbox": "*",
|
||||
"@radix-ui/react-label": "*",
|
||||
"@radix-ui/react-progress": "*",
|
||||
"@radix-ui/react-slot": "*",
|
||||
"@radix-ui/react-tooltip": "*",
|
||||
"@react-router/fs-routes": "^7.4.0",
|
||||
"@supabase/postgrest-js": "*",
|
||||
"@supabase/supa-mdx-lint": "0.2.6-alpha",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"class-variance-authority": "*",
|
||||
"common": "workspace:*",
|
||||
"contentlayer2": "0.4.6",
|
||||
"eslint-config-supabase": "workspace:*",
|
||||
|
||||
19
apps/ui-library/public/r/infinite-query-hook.json
Normal file
19
apps/ui-library/public/r/infinite-query-hook.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1115,6 +1115,21 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "infinite-query-hook",
|
||||
"type": "registry:block",
|
||||
"title": "Infinite Query Hook",
|
||||
"description": "React hook for infinite lists, fetching data from Supabase.",
|
||||
"dependencies": ["@supabase/supabase-js", "@supabase/postgrest-js@*"],
|
||||
"registryDependencies": [],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "supabase-client-nextjs",
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { type Registry, type RegistryItem } from 'shadcn/registry'
|
||||
import { clients } from './clients'
|
||||
import { registryItemAppend } from './utils'
|
||||
|
||||
import currentUserAvatar from './default/blocks/current-user-avatar/registry-item.json' with { type: 'json' }
|
||||
import dropzone from './default/blocks/dropzone/registry-item.json' with { type: 'json' }
|
||||
import infiniteQueryHook from './default/blocks/infinite-query-hook/registry-item.json' with { type: 'json' }
|
||||
import realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry-item.json' with { type: 'json' }
|
||||
import realtimeChat from './default/blocks/realtime-chat/registry-item.json' with { type: 'json' }
|
||||
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' with { type: 'json' }
|
||||
|
||||
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' with { type: 'json' }
|
||||
import passwordBasedAuthReactRouter from './default/blocks/password-based-auth-react-router/registry-item.json' with { type: 'json' }
|
||||
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' with { type: 'json' }
|
||||
@@ -12,12 +19,6 @@ import socialAuthReactRouter from './default/blocks/social-auth-react-router/reg
|
||||
import socialAuthReact from './default/blocks/social-auth-react/registry-item.json' with { type: 'json' }
|
||||
import socialAuthTanstack from './default/blocks/social-auth-tanstack/registry-item.json' with { type: 'json' }
|
||||
|
||||
import realtimeAvatarStack from './default/blocks/realtime-avatar-stack/registry-item.json' with { type: 'json' }
|
||||
|
||||
import realtimeChat from './default/blocks/realtime-chat/registry-item.json' with { type: 'json' }
|
||||
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' with { type: 'json' }
|
||||
import { registryItemAppend } from './utils'
|
||||
|
||||
const combine = (component: Registry['items'][number]) => {
|
||||
return clients.flatMap((client) => {
|
||||
return registryItemAppend(
|
||||
@@ -51,4 +52,6 @@ export const blocks = [
|
||||
...combine(currentUserAvatar as RegistryItem),
|
||||
...combine(realtimeAvatarStack as RegistryItem),
|
||||
...combine(realtimeChat as RegistryItem),
|
||||
// infinite query hook is intentionally not combined with the clients since it depends on clients having database types.
|
||||
infiniteQueryHook as RegistryItem,
|
||||
] as Registry['items']
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
'use client'
|
||||
|
||||
import { createClient } from '@/registry/default/fixtures/lib/supabase/client'
|
||||
import { PostgrestQueryBuilder } from '@supabase/postgrest-js'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { useEffect, useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
// The following types are used to make the hook type-safe. It extracts the database type from the supabase client.
|
||||
type SupabaseClientType = typeof supabase
|
||||
|
||||
// Utility type to check if the type is any
|
||||
type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N
|
||||
|
||||
// Extracts the database type from the supabase client. If the supabase client doesn't have a type, it will fallback properly.
|
||||
type Database =
|
||||
SupabaseClientType extends SupabaseClient<infer U>
|
||||
? IfAny<
|
||||
U,
|
||||
{
|
||||
public: {
|
||||
Tables: Record<string, any>
|
||||
Views: Record<string, any>
|
||||
Functions: Record<string, any>
|
||||
}
|
||||
},
|
||||
U
|
||||
>
|
||||
: never
|
||||
|
||||
// Change this to the database schema you want to use
|
||||
type DatabaseSchema = Database['public']
|
||||
|
||||
// Extracts the table names from the database type
|
||||
type SupabaseTableName = keyof DatabaseSchema['Tables']
|
||||
|
||||
// Extracts the table definition from the database type
|
||||
type SupabaseTableData<T extends SupabaseTableName> = DatabaseSchema['Tables'][T]['Row']
|
||||
|
||||
type SupabaseSelectBuilder<T extends SupabaseTableName> = ReturnType<
|
||||
PostgrestQueryBuilder<DatabaseSchema, DatabaseSchema['Tables'][T], T>['select']
|
||||
>
|
||||
|
||||
// A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.
|
||||
type SupabaseQueryHandler<T extends SupabaseTableName> = (
|
||||
query: SupabaseSelectBuilder<T>
|
||||
) => SupabaseSelectBuilder<T>
|
||||
|
||||
interface UseInfiniteQueryProps<T extends SupabaseTableName, Query extends string = '*'> {
|
||||
// The table name to query
|
||||
tableName: T
|
||||
// The columns to select, defaults to `*`
|
||||
columns?: string
|
||||
// The number of items to fetch per page, defaults to `20`
|
||||
pageSize?: number
|
||||
// A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.
|
||||
trailingQuery?: SupabaseQueryHandler<T>
|
||||
}
|
||||
|
||||
interface StoreState<TData> {
|
||||
data: TData[]
|
||||
count: number
|
||||
isSuccess: boolean
|
||||
isLoading: boolean
|
||||
isFetching: boolean
|
||||
error: Error | null
|
||||
hasInitialFetch: boolean
|
||||
}
|
||||
|
||||
type Listener = () => void
|
||||
|
||||
function createStore<TData extends SupabaseTableData<T>, T extends SupabaseTableName>(
|
||||
props: UseInfiniteQueryProps<T>
|
||||
) {
|
||||
const { tableName, columns = '*', pageSize = 20, trailingQuery } = props
|
||||
|
||||
let state: StoreState<TData> = {
|
||||
data: [],
|
||||
count: 0,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
error: null,
|
||||
hasInitialFetch: false,
|
||||
}
|
||||
|
||||
const listeners = new Set<Listener>()
|
||||
|
||||
const notify = () => {
|
||||
listeners.forEach((listener) => listener())
|
||||
}
|
||||
|
||||
const setState = (newState: Partial<StoreState<TData>>) => {
|
||||
state = { ...state, ...newState }
|
||||
notify()
|
||||
}
|
||||
|
||||
const fetchPage = async (skip: number) => {
|
||||
if (state.hasInitialFetch && (state.isFetching || state.count <= state.data.length)) return
|
||||
|
||||
setState({ isFetching: true })
|
||||
|
||||
let query = supabase
|
||||
.from(tableName)
|
||||
.select(columns, { count: 'exact' }) as unknown as SupabaseSelectBuilder<T>
|
||||
|
||||
if (trailingQuery) {
|
||||
query = trailingQuery(query)
|
||||
}
|
||||
const { data: newData, count, error } = await query.range(skip, skip + pageSize - 1)
|
||||
|
||||
if (error) {
|
||||
console.error('An unexpected error occurred:', error)
|
||||
setState({ error })
|
||||
} else {
|
||||
const deduplicatedData = ((newData || []) as TData[]).filter(
|
||||
(item) => !state.data.find((old) => old.id === item.id)
|
||||
)
|
||||
|
||||
setState({
|
||||
data: [...state.data, ...deduplicatedData],
|
||||
count: count || 0,
|
||||
isSuccess: true,
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
setState({ isFetching: false })
|
||||
}
|
||||
|
||||
const fetchNextPage = async () => {
|
||||
if (state.isFetching) return
|
||||
await fetchPage(state.data.length)
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
setState({ isLoading: true, isSuccess: false, data: [] })
|
||||
await fetchNextPage()
|
||||
setState({ isLoading: false, hasInitialFetch: true })
|
||||
}
|
||||
|
||||
return {
|
||||
getState: () => state,
|
||||
subscribe: (listener: Listener) => {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
},
|
||||
fetchNextPage,
|
||||
initialize,
|
||||
}
|
||||
}
|
||||
|
||||
// Empty initial state to avoid hydration errors.
|
||||
const initialState: any = {
|
||||
data: [],
|
||||
count: 0,
|
||||
isSuccess: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
error: null,
|
||||
hasInitialFetch: false,
|
||||
}
|
||||
|
||||
function useInfiniteQuery<
|
||||
TData extends SupabaseTableData<T>,
|
||||
T extends SupabaseTableName = SupabaseTableName,
|
||||
>(props: UseInfiniteQueryProps<T>) {
|
||||
const storeRef = useRef(createStore<TData, T>(props))
|
||||
|
||||
const state = useSyncExternalStore(
|
||||
storeRef.current.subscribe,
|
||||
() => storeRef.current.getState(),
|
||||
() => initialState as StoreState<TData>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Recreate store if props change
|
||||
if (
|
||||
storeRef.current.getState().hasInitialFetch &&
|
||||
(props.tableName !== props.tableName ||
|
||||
props.columns !== props.columns ||
|
||||
props.pageSize !== props.pageSize)
|
||||
) {
|
||||
storeRef.current = createStore<TData, T>(props)
|
||||
}
|
||||
|
||||
if (!state.hasInitialFetch && typeof window !== 'undefined') {
|
||||
storeRef.current.initialize()
|
||||
}
|
||||
}, [props.tableName, props.columns, props.pageSize, state.hasInitialFetch])
|
||||
|
||||
return {
|
||||
data: state.data,
|
||||
count: state.count,
|
||||
isSuccess: state.isSuccess,
|
||||
isLoading: state.isLoading,
|
||||
isFetching: state.isFetching,
|
||||
error: state.error,
|
||||
hasMore: state.count > state.data.length,
|
||||
fetchNextPage: storeRef.current.fetchNextPage,
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
useInfiniteQuery,
|
||||
type SupabaseQueryHandler,
|
||||
type SupabaseTableData,
|
||||
type SupabaseTableName,
|
||||
type UseInfiniteQueryProps,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "infinite-query-hook",
|
||||
"type": "registry:block",
|
||||
"title": "Infinite Query Hook",
|
||||
"description": "React hook for infinite lists, fetching data from Supabase.",
|
||||
"dependencies": ["@supabase/supabase-js", "@supabase/postgrest-js@*"],
|
||||
"registryDependencies": [],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
apps/ui-library/registry/default/components/ui/checkbox.tsx
Normal file
28
apps/ui-library/registry/default/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { Check } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,7 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('animate-pulse rounded-md bg-primary/10', className)} {...props} />
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,149 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
SupabaseQueryHandler,
|
||||
SupabaseTableData,
|
||||
SupabaseTableName,
|
||||
useInfiniteQuery,
|
||||
} from '@/registry/default/blocks/infinite-query-hook/hooks/use-infinite-query'
|
||||
import { Checkbox } from '@/registry/default/components/ui/checkbox'
|
||||
import { Database } from '@/registry/default/fixtures/database.types'
|
||||
import * as React from 'react'
|
||||
|
||||
interface InfiniteListProps<TableName extends SupabaseTableName> {
|
||||
tableName: TableName
|
||||
columns?: string
|
||||
pageSize?: number
|
||||
trailingQuery?: SupabaseQueryHandler<TableName>
|
||||
renderItem: (item: SupabaseTableData<TableName>, index: number) => React.ReactNode
|
||||
className?: string
|
||||
renderNoResults?: () => React.ReactNode
|
||||
renderEndMessage?: () => React.ReactNode
|
||||
renderSkeleton?: (count: number) => React.ReactNode
|
||||
}
|
||||
|
||||
const DefaultNoResults = () => (
|
||||
<div className="text-center text-muted-foreground py-10">No results.</div>
|
||||
)
|
||||
|
||||
const DefaultEndMessage = () => (
|
||||
<div className="text-center text-muted-foreground py-4 text-sm">You've reached the end.</div>
|
||||
)
|
||||
|
||||
const defaultSkeleton = (count: number) => (
|
||||
<div className="flex flex-col gap-2 px-4">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div key={index} className="h-4 w-full bg-muted animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
export function InfiniteList<TableName extends SupabaseTableName>({
|
||||
tableName,
|
||||
columns = '*',
|
||||
pageSize = 20,
|
||||
trailingQuery,
|
||||
renderItem,
|
||||
className,
|
||||
renderNoResults = DefaultNoResults,
|
||||
renderEndMessage = DefaultEndMessage,
|
||||
renderSkeleton = defaultSkeleton,
|
||||
}: InfiniteListProps<TableName>) {
|
||||
const { data, isFetching, hasMore, fetchNextPage, isSuccess } = useInfiniteQuery({
|
||||
tableName,
|
||||
columns,
|
||||
pageSize,
|
||||
trailingQuery,
|
||||
})
|
||||
|
||||
// Ref for the scrolling container
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Intersection observer logic - target the last rendered *item* or a dedicated sentinel
|
||||
const loadMoreSentinelRef = React.useRef<HTMLDivElement>(null)
|
||||
const observer = React.useRef<IntersectionObserver | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (observer.current) observer.current.disconnect()
|
||||
|
||||
observer.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore && !isFetching) {
|
||||
fetchNextPage()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainerRef.current, // Use the scroll container for scroll detection
|
||||
threshold: 0.1, // Trigger when 10% of the target is visible
|
||||
rootMargin: '0px 0px 100px 0px', // Trigger loading a bit before reaching the end
|
||||
}
|
||||
)
|
||||
|
||||
if (loadMoreSentinelRef.current) {
|
||||
observer.current.observe(loadMoreSentinelRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observer.current) observer.current.disconnect()
|
||||
}
|
||||
}, [isFetching, hasMore, fetchNextPage])
|
||||
|
||||
return (
|
||||
<div ref={scrollContainerRef} className={cn('relative h-full overflow-auto', className)}>
|
||||
<div>
|
||||
{isSuccess && data.length === 0 && renderNoResults()}
|
||||
|
||||
{data.map((item, index) => renderItem(item, index))}
|
||||
|
||||
{isFetching && renderSkeleton && renderSkeleton(pageSize)}
|
||||
|
||||
<div ref={loadMoreSentinelRef} style={{ height: '1px' }} />
|
||||
|
||||
{!hasMore && data.length > 0 && renderEndMessage()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TodoTask = Database['public']['Tables']['todos']['Row']
|
||||
|
||||
// Define how each item should be rendered
|
||||
const renderTodoItem = (todo: TodoTask) => {
|
||||
return (
|
||||
<div
|
||||
key={todo.id}
|
||||
className="border-b py-3 px-4 hover:bg-muted flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox defaultChecked={todo.is_complete ?? false} />
|
||||
<div>
|
||||
<span className="font-medium text-sm text-foreground">{todo.task}</span>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{new Date(todo.inserted_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Define a filter to only show logs with log_level = 'info'
|
||||
const orderByInsertedAt: SupabaseQueryHandler<'todos'> = (query) => {
|
||||
return query.order('inserted_at', { ascending: false })
|
||||
}
|
||||
|
||||
const InfiniteListDemo = () => {
|
||||
return (
|
||||
<div className="bg-background h-[600px]">
|
||||
<InfiniteList
|
||||
tableName="todos"
|
||||
renderItem={renderTodoItem}
|
||||
pageSize={3}
|
||||
trailingQuery={orderByInsertedAt}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfiniteListDemo
|
||||
179
apps/ui-library/registry/default/fixtures/database.types.ts
Normal file
179
apps/ui-library/registry/default/fixtures/database.types.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
||||
|
||||
export type Database = {
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
graphql: {
|
||||
Args: {
|
||||
operationName?: string
|
||||
query?: string
|
||||
variables?: Json
|
||||
extensions?: Json
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
todos: {
|
||||
Row: {
|
||||
id: number
|
||||
inserted_at: string
|
||||
is_complete: boolean | null
|
||||
task: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
inserted_at?: string
|
||||
is_complete?: boolean | null
|
||||
task?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
inserted_at?: string
|
||||
is_complete?: boolean | null
|
||||
task?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, 'public'>]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
|
||||
? (DefaultSchema['Tables'] & DefaultSchema['Views'])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema['Tables']
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
|
||||
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema['Tables']
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema['Tables']
|
||||
? DefaultSchema['Tables'][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums'] | { schema: keyof Database },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
|
||||
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema['CompositeTypes']
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
|
||||
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
||||
export const Constants = {
|
||||
graphql_public: {
|
||||
Enums: {},
|
||||
},
|
||||
public: {
|
||||
Enums: {},
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
import { Database } from '../../database.types'
|
||||
// This client is meant to be used for demo purposes only. It has types from the Supabase project in the ui-library app.
|
||||
export function createClient() {
|
||||
return createBrowserClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
}
|
||||
@@ -78,4 +78,15 @@ export const examples: Registry['items'] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'infinite-query-hook-demo',
|
||||
type: 'registry:example',
|
||||
registryDependencies: [],
|
||||
files: [
|
||||
{
|
||||
path: 'registry/default/examples/infinite-query-hook-demo.tsx',
|
||||
type: 'registry:example',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
create table "public"."todos" (
|
||||
"id" bigint generated by default as identity not null,
|
||||
"user_id" uuid not null,
|
||||
"task" text,
|
||||
"is_complete" boolean default false,
|
||||
"inserted_at" timestamp with time zone not null default timezone('utc'::text, now())
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."todos" enable row level security;
|
||||
|
||||
CREATE UNIQUE INDEX todos_pkey ON public.todos USING btree (id);
|
||||
|
||||
alter table "public"."todos" add constraint "todos_pkey" PRIMARY KEY using index "todos_pkey";
|
||||
|
||||
alter table "public"."todos" add constraint "todos_task_check" CHECK ((char_length(task) > 3)) not valid;
|
||||
|
||||
alter table "public"."todos" validate constraint "todos_task_check";
|
||||
|
||||
alter table "public"."todos" add constraint "todos_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid;
|
||||
|
||||
alter table "public"."todos" validate constraint "todos_user_id_fkey";
|
||||
|
||||
grant delete on table "public"."todos" to "anon";
|
||||
|
||||
grant insert on table "public"."todos" to "anon";
|
||||
|
||||
grant references on table "public"."todos" to "anon";
|
||||
|
||||
grant select on table "public"."todos" to "anon";
|
||||
|
||||
grant trigger on table "public"."todos" to "anon";
|
||||
|
||||
grant truncate on table "public"."todos" to "anon";
|
||||
|
||||
grant update on table "public"."todos" to "anon";
|
||||
|
||||
grant delete on table "public"."todos" to "authenticated";
|
||||
|
||||
grant insert on table "public"."todos" to "authenticated";
|
||||
|
||||
grant references on table "public"."todos" to "authenticated";
|
||||
|
||||
grant select on table "public"."todos" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."todos" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."todos" to "authenticated";
|
||||
|
||||
grant update on table "public"."todos" to "authenticated";
|
||||
|
||||
grant delete on table "public"."todos" to "service_role";
|
||||
|
||||
grant insert on table "public"."todos" to "service_role";
|
||||
|
||||
grant references on table "public"."todos" to "service_role";
|
||||
|
||||
grant select on table "public"."todos" to "service_role";
|
||||
|
||||
grant trigger on table "public"."todos" to "service_role";
|
||||
|
||||
grant truncate on table "public"."todos" to "service_role";
|
||||
|
||||
grant update on table "public"."todos" to "service_role";
|
||||
|
||||
create policy "Individuals can create todos."
|
||||
on "public"."todos"
|
||||
as permissive
|
||||
for insert
|
||||
to public
|
||||
with check ((auth.uid() = user_id));
|
||||
|
||||
|
||||
create policy "Individuals can delete their own todos."
|
||||
on "public"."todos"
|
||||
as permissive
|
||||
for delete
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
create policy "Individuals can update their own todos."
|
||||
on "public"."todos"
|
||||
as permissive
|
||||
for update
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
create policy "Everyone can read all todos. "
|
||||
on "public"."todos"
|
||||
as permissive
|
||||
for select
|
||||
to public
|
||||
using (true);
|
||||
|
||||
|
||||
|
||||
|
||||
239
apps/ui-library/supabase/seed.sql
Normal file
239
apps/ui-library/supabase/seed.sql
Normal file
@@ -0,0 +1,239 @@
|
||||
insert into "auth"."users"
|
||||
(
|
||||
"instance_id",
|
||||
"id",
|
||||
"aud",
|
||||
"role",
|
||||
"email",
|
||||
"encrypted_password",
|
||||
"email_confirmed_at",
|
||||
"invited_at",
|
||||
"recovery_token",
|
||||
"recovery_sent_at",
|
||||
"last_sign_in_at",
|
||||
"raw_app_meta_data",
|
||||
"raw_user_meta_data",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
values
|
||||
(
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'authenticated',
|
||||
'authenticated',
|
||||
'test@test.com',
|
||||
'$2a$10$rIvxpnRv5waKFZIQFpMJke079cHjJlqACLZXaONomkc4FaZ4Btlbe',
|
||||
'2024-02-03 23:38:34.499444+00',
|
||||
'2024-02-03 23:38:21.438042+00',
|
||||
'a73278d79e14c427ad5d21509fe88963d258377f77fe9e268d0a92ed',
|
||||
'2025-01-18 13:35:30.6347+00',
|
||||
'2025-02-09 14:27:57.653171+00',
|
||||
'{"provider": "email", "providers": ["email"]}',
|
||||
'{}',
|
||||
'2024-02-03 23:38:21.431361+00',
|
||||
'2025-04-16 20:04:09.697799+00'
|
||||
);
|
||||
|
||||
insert into "public"."todos"
|
||||
("id", "user_id", "task", "is_complete", "inserted_at")
|
||||
values
|
||||
(1, '8999b3f3-6465-4135-9b4f-42c750b90ffb', 'Test task', true, '2024-03-20 14:23:45.000000+00'),
|
||||
(2, '8999b3f3-6465-4135-9b4f-42c750b90ffb', 'Do laundry', false, '2024-03-15 09:12:30.000000+00'),
|
||||
(
|
||||
3,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean bathroom',
|
||||
true,
|
||||
'2024-03-28 16:45:22.000000+00'
|
||||
),
|
||||
(
|
||||
4,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Vacuum living room',
|
||||
false,
|
||||
'2024-03-10 11:30:15.000000+00'
|
||||
),
|
||||
(
|
||||
5,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Mow the lawn',
|
||||
true,
|
||||
'2024-03-22 08:20:40.000000+00'
|
||||
),
|
||||
(
|
||||
6,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Wash dishes',
|
||||
false,
|
||||
'2024-03-17 19:05:33.000000+00'
|
||||
),
|
||||
(
|
||||
7,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean kitchen countertops',
|
||||
true,
|
||||
'2024-03-25 13:40:18.000000+00'
|
||||
),
|
||||
(
|
||||
8,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Take out trash',
|
||||
false,
|
||||
'2024-03-12 07:55:27.000000+00'
|
||||
),
|
||||
(
|
||||
9,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Organize closet',
|
||||
true,
|
||||
'2024-03-19 10:15:42.000000+00'
|
||||
),
|
||||
(
|
||||
10,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean windows',
|
||||
false,
|
||||
'2024-03-27 15:30:55.000000+00'
|
||||
),
|
||||
(
|
||||
11,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Water plants',
|
||||
true,
|
||||
'2024-03-14 12:45:10.000000+00'
|
||||
),
|
||||
(
|
||||
12,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean refrigerator',
|
||||
false,
|
||||
'2024-03-23 17:20:35.000000+00'
|
||||
),
|
||||
(
|
||||
13,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Dust furniture',
|
||||
true,
|
||||
'2024-03-16 14:10:25.000000+00'
|
||||
),
|
||||
(
|
||||
14,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean oven',
|
||||
false,
|
||||
'2024-03-29 09:35:50.000000+00'
|
||||
),
|
||||
(
|
||||
15,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Sweep floors',
|
||||
true,
|
||||
'2024-03-11 16:50:15.000000+00'
|
||||
),
|
||||
(
|
||||
16,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean microwave',
|
||||
false,
|
||||
'2024-03-24 08:25:40.000000+00'
|
||||
),
|
||||
(
|
||||
17,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Organize pantry',
|
||||
true,
|
||||
'2024-03-18 13:40:30.000000+00'
|
||||
),
|
||||
(
|
||||
18,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean shower',
|
||||
false,
|
||||
'2024-03-26 11:15:45.000000+00'
|
||||
),
|
||||
(
|
||||
19,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Wipe down baseboards',
|
||||
true,
|
||||
'2024-03-13 10:30:20.000000+00'
|
||||
),
|
||||
(
|
||||
20,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean ceiling fans',
|
||||
false,
|
||||
'2024-03-21 15:45:55.000000+00'
|
||||
),
|
||||
(
|
||||
21,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Organize garage',
|
||||
true,
|
||||
'2024-03-30 07:20:10.000000+00'
|
||||
),
|
||||
(
|
||||
22,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean gutters',
|
||||
false,
|
||||
'2024-03-09 14:35:25.000000+00'
|
||||
),
|
||||
(23, '8999b3f3-6465-4135-9b4f-42c750b90ffb', 'Wash car', true, '2024-03-31 12:50:40.000000+00'),
|
||||
(
|
||||
24,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean pet bedding',
|
||||
false,
|
||||
'2024-03-08 09:05:15.000000+00'
|
||||
),
|
||||
(
|
||||
25,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Organize bookshelf',
|
||||
true,
|
||||
'2024-03-07 16:20:30.000000+00'
|
||||
),
|
||||
(
|
||||
26,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean air filters',
|
||||
false,
|
||||
'2024-03-06 13:35:45.000000+00'
|
||||
),
|
||||
(
|
||||
27,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean light fixtures',
|
||||
true,
|
||||
'2024-03-05 10:50:10.000000+00'
|
||||
),
|
||||
(
|
||||
28,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Organize desk',
|
||||
false,
|
||||
'2024-03-04 08:05:25.000000+00'
|
||||
),
|
||||
(
|
||||
29,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean patio furniture',
|
||||
true,
|
||||
'2024-03-03 15:20:40.000000+00'
|
||||
),
|
||||
(
|
||||
30,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Clean door handles',
|
||||
false,
|
||||
'2024-03-02 12:35:55.000000+00'
|
||||
),
|
||||
(
|
||||
31,
|
||||
'8999b3f3-6465-4135-9b4f-42c750b90ffb',
|
||||
'Organize medicine cabinet',
|
||||
true,
|
||||
'2024-03-01 09:50:20.000000+00'
|
||||
);
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -1011,28 +1011,34 @@ importers:
|
||||
apps/ui-library:
|
||||
dependencies:
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: ^1.0.4
|
||||
specifier: '*'
|
||||
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: '*'
|
||||
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.0.2
|
||||
specifier: '*'
|
||||
version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.0.3
|
||||
specifier: '*'
|
||||
version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.2
|
||||
specifier: '*'
|
||||
version: 1.1.2(@types/react@18.3.3)(react@18.3.1)
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.0.7
|
||||
specifier: '*'
|
||||
version: 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@react-router/fs-routes':
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2)
|
||||
'@supabase/postgrest-js':
|
||||
specifier: '*'
|
||||
version: 1.19.2
|
||||
'@supabase/supa-mdx-lint':
|
||||
specifier: 0.2.6-alpha
|
||||
version: 0.2.6-alpha
|
||||
class-variance-authority:
|
||||
specifier: ^0.6.0
|
||||
specifier: '*'
|
||||
version: 0.6.1
|
||||
common:
|
||||
specifier: workspace:*
|
||||
|
||||
Reference in New Issue
Block a user