diff --git a/examples/react/bi-direction-infinite-query/.gitignore b/examples/react/bi-direction-infinite-query/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/examples/react/bi-direction-infinite-query/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/react/bi-direction-infinite-query/README.md b/examples/react/bi-direction-infinite-query/README.md new file mode 100644 index 0000000..0dc9ea2 --- /dev/null +++ b/examples/react/bi-direction-infinite-query/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +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. diff --git a/examples/react/bi-direction-infinite-query/app/api/projects/route.js b/examples/react/bi-direction-infinite-query/app/api/projects/route.js new file mode 100644 index 0000000..f00bd78 --- /dev/null +++ b/examples/react/bi-direction-infinite-query/app/api/projects/route.js @@ -0,0 +1,24 @@ +import { NextResponse } from 'next/server'; + +export async function GET(request) { + const searchParams = request.nextUrl.searchParams; + + const cursor = Number(searchParams.get('cursor') || 0); + const pageSize = 5; + + const data = Array(pageSize) + .fill(0) + .map((_, i) => { + return { + name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`, + id: i + cursor, + }; + }); + + const nextId = cursor < 10 ? data[data.length - 1].id + 1 : null; + const previousId = cursor > -10 ? data[0].id - pageSize : null; + + await new Promise((r) => setTimeout(r, 1000)); // Simulate delay + + return NextResponse.json({ data, nextId, previousId }); +} diff --git a/examples/react/bi-direction-infinite-query/app/layout.js b/examples/react/bi-direction-infinite-query/app/layout.js new file mode 100644 index 0000000..cd29ec9 --- /dev/null +++ b/examples/react/bi-direction-infinite-query/app/layout.js @@ -0,0 +1,11 @@ +export const metadata = { + title: 'Dependent Queries | Floppy Disk', +}; + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/examples/react/bi-direction-infinite-query/app/page.js b/examples/react/bi-direction-infinite-query/app/page.js new file mode 100644 index 0000000..fde7aa6 --- /dev/null +++ b/examples/react/bi-direction-infinite-query/app/page.js @@ -0,0 +1,60 @@ +'use client'; + +import { createBiDirectionQuery } from 'floppy-disk'; + +const fetchProjects = async (cursor) => { + const res = await fetch(`/api/projects?cursor=${cursor}`); + const resJson = await res.json(); + if (res.ok) return resJson; + throw resJson; +}; + +const useProjectsQuery = createBiDirectionQuery( + (queryKey, { pageParam }, direction) => fetchProjects(pageParam || 0), + { + select: (response, { data = [] }, direction) => { + return direction === 'prev' ? response.data.concat(data) : data.concat(response.data); + }, + getPrevPageParam: (response) => response.previousId, + getNextPageParam: (response) => response.nextId, + }, +); + +export default function BiDirectionPage() { + const { + data, + fetchPrevPage, + hasPrevPage, + isWaitingPrevPage, + fetchNextPage, + hasNextPage, + isWaitingNextPage, + } = useProjectsQuery(); + + return ( +
+

Bi-Direction Infinite Query

+ +
+ + + +
+ ); +} diff --git a/examples/react/bi-direction-infinite-query/next.config.js b/examples/react/bi-direction-infinite-query/next.config.js new file mode 100644 index 0000000..767719f --- /dev/null +++ b/examples/react/bi-direction-infinite-query/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/examples/react/bi-direction-infinite-query/package.json b/examples/react/bi-direction-infinite-query/package.json new file mode 100644 index 0000000..73e5fd8 --- /dev/null +++ b/examples/react/bi-direction-infinite-query/package.json @@ -0,0 +1,17 @@ +{ + "name": "floppy-disk-example", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "floppy-disk": "^2.6.0", + "next": "13.5.2", + "react": "18.2.0", + "react-dom": "18.2.0" + } +} diff --git a/examples/react/dependent-queries/.gitignore b/examples/react/dependent-queries/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/examples/react/dependent-queries/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/react/dependent-queries/README.md b/examples/react/dependent-queries/README.md new file mode 100644 index 0000000..0dc9ea2 --- /dev/null +++ b/examples/react/dependent-queries/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +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. diff --git a/examples/react/dependent-queries/app/api/cities/route.js b/examples/react/dependent-queries/app/api/cities/route.js new file mode 100644 index 0000000..86b8c62 --- /dev/null +++ b/examples/react/dependent-queries/app/api/cities/route.js @@ -0,0 +1,34 @@ +import { NextResponse } from 'next/server'; + +import { countries, paginate } from '../data'; + +export async function GET(request) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get('q')?.toLocaleLowerCase(); + const countryId = searchParams.get('countryId'); + const provinceId = searchParams.get('provinceId'); + if (!query || !countryId || !provinceId) { + return NextResponse.json( + { message: 'Please provide country id, province id, & query parameter' }, + { status: 400 }, + ); + } + + const provinces = countries.find((item) => item.id === countryId)?.provinces; + if (!provinces) { + return NextResponse.json({ message: `Country id "${countryId}" not found` }, { status: 404 }); + } + + const cities = provinces.find((item) => item.id === provinceId)?.cities; + if (!cities) { + return NextResponse.json({ message: `Province id "${provinceId}" not found` }, { status: 404 }); + } + + const { records, pagination } = paginate( + cities.filter((item) => item.name.toLocaleLowerCase().includes(query)), + ); + return NextResponse.json({ + records: records.map(({ id, name }) => ({ id, name })), + pagination, + }); +} diff --git a/examples/react/dependent-queries/app/api/countries/route.js b/examples/react/dependent-queries/app/api/countries/route.js new file mode 100644 index 0000000..e65ab3f --- /dev/null +++ b/examples/react/dependent-queries/app/api/countries/route.js @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; + +import { countries, paginate } from '../data'; + +export async function GET(request) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get('q')?.toLocaleLowerCase(); + if (!query) { + return NextResponse.json({ message: 'Please provide a query parameter' }, { status: 400 }); + } + + const { records, pagination } = paginate( + countries.filter((item) => item.name.toLocaleLowerCase().includes(query)), + ); + return NextResponse.json({ + records: records.map(({ id, name }) => ({ id, name })), + pagination, + }); +} diff --git a/examples/react/dependent-queries/app/api/data.js b/examples/react/dependent-queries/app/api/data.js new file mode 100644 index 0000000..f22a4d5 --- /dev/null +++ b/examples/react/dependent-queries/app/api/data.js @@ -0,0 +1,31 @@ +import { faker } from '@faker-js/faker'; + +export const paginate = (data, page = 1, limit = 10) => { + return { + records: data.slice((page - 1) * limit, page * limit), + pagination: { + currentPage: page, + totalPages: Math.ceil(data.length / limit), + totalRecords: data.length, + }, + }; +}; + +export const countries = [...Array(50)].map(() => { + return { + id: faker.string.alphanumeric(8), + name: faker.lorem.words(2), + provinces: [...Array(50)].map(() => { + return { + id: faker.string.alphanumeric(8), + name: faker.lorem.words(2), + cities: [...Array(50)].map(() => { + return { + id: faker.string.alphanumeric(8), + name: faker.lorem.words(2), + }; + }), + }; + }), + }; +}); diff --git a/examples/react/dependent-queries/app/api/provinces/route.js b/examples/react/dependent-queries/app/api/provinces/route.js new file mode 100644 index 0000000..40c2b99 --- /dev/null +++ b/examples/react/dependent-queries/app/api/provinces/route.js @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server'; + +import { countries, paginate } from '../data'; + +export async function GET(request) { + const searchParams = request.nextUrl.searchParams; + const query = searchParams.get('q')?.toLocaleLowerCase(); + const countryId = searchParams.get('countryId'); + if (!query || !countryId) { + return NextResponse.json( + { message: 'Please provide country id & query parameter' }, + { status: 400 }, + ); + } + + const provinces = countries.find((item) => item.id === countryId)?.provinces; + if (!provinces) { + return NextResponse.json({ message: `Country id "${countryId}" not found` }, { status: 404 }); + } + + const { records, pagination } = paginate( + provinces.filter((item) => item.name.toLocaleLowerCase().includes(query)), + ); + return NextResponse.json({ + records: records.map(({ id, name }) => ({ id, name })), + pagination, + }); +} diff --git a/examples/react/dependent-queries/app/layout.js b/examples/react/dependent-queries/app/layout.js new file mode 100644 index 0000000..cd29ec9 --- /dev/null +++ b/examples/react/dependent-queries/app/layout.js @@ -0,0 +1,11 @@ +export const metadata = { + title: 'Dependent Queries | Floppy Disk', +}; + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/examples/react/dependent-queries/app/page.js b/examples/react/dependent-queries/app/page.js new file mode 100644 index 0000000..b598c3c --- /dev/null +++ b/examples/react/dependent-queries/app/page.js @@ -0,0 +1,85 @@ +'use client'; + +import { useState } from 'react'; +import { useDebounce } from 'react-power-ups'; + +import { useCitiesQuery, useCountriesQuery, useProvincesQuery } from './queries'; + +export default function AddressForm() { + const [country, setCountry] = useState(); + const [province, setProvince] = useState(); + const [city, setCity] = useState(); + + return ( +
+

Dependent Queries Example

+ + + + +
+ ); +} + +function Dropdown({ title, value, onChange, useQuery, queryParams }) { + const [keyword, setKeyword] = useState(''); + const debouncedKeyword = useDebounce(keyword, 2000); + const query = useQuery({ q: debouncedKeyword, ...queryParams }); + return ( +
+

{title}

+
Selected: {JSON.stringify(value) || '-'}
+
+ setKeyword(e.target.value)} + /> +
+ {!!keyword && } +
+ ); +} + +function DropdownMenu({ query, onChange }) { + const { isLoading, isSuccess, error, data = [] } = query; + + if (isLoading) { + return
Loading... ⏳
; + } + + if (isSuccess) { + return data.length ? ( + + ) : ( +
No data found
+ ); + } + + return
Error: {JSON.stringify(error, null, 2)}
; +} diff --git a/examples/react/dependent-queries/app/queries.js b/examples/react/dependent-queries/app/queries.js new file mode 100644 index 0000000..98a38e7 --- /dev/null +++ b/examples/react/dependent-queries/app/queries.js @@ -0,0 +1,37 @@ +import { createQuery } from 'floppy-disk'; + +const getCountries = async ({ q }) => { + const res = await fetch(`/api/countries?q=${encodeURIComponent(q)}`); + const resJson = await res.json(); + if (res.ok) return resJson; + throw resJson; +}; + +const getProvinces = async ({ q, countryId }) => { + const res = await fetch(`/api/provinces?q=${encodeURIComponent(q)}&countryId=${countryId}`); + const resJson = await res.json(); + if (res.ok) return resJson; + throw resJson; +}; + +const getCities = async ({ q, countryId, provinceId }) => { + const res = await fetch( + `/api/countries?q=${encodeURIComponent(q)}&countryId=${countryId}&provinceId=${provinceId}`, + ); + const resJson = await res.json(); + if (res.ok) return resJson; + throw resJson; +}; + +export const useCountriesQuery = createQuery(getCountries, { + select: (response) => response.records, + enabled: ({ q }) => q, +}); +export const useProvincesQuery = createQuery(getProvinces, { + select: (response) => response.records, + enabled: ({ q, countryId }) => q && countryId, +}); +export const useCitiesQuery = createQuery(getCities, { + select: (response) => response.records, + enabled: ({ q, countryId, provinceId }) => q && countryId && provinceId, +}); diff --git a/examples/react/dependent-queries/next.config.js b/examples/react/dependent-queries/next.config.js new file mode 100644 index 0000000..767719f --- /dev/null +++ b/examples/react/dependent-queries/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/examples/react/dependent-queries/package.json b/examples/react/dependent-queries/package.json new file mode 100644 index 0000000..dce9b16 --- /dev/null +++ b/examples/react/dependent-queries/package.json @@ -0,0 +1,19 @@ +{ + "name": "floppy-disk-example", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@faker-js/faker": "^8.1.0", + "floppy-disk": "^2.5.0", + "next": "13.5.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-power-ups": "^3.1.3" + } +}