diff --git a/lib/generator/templates/.eslintrc.json b/lib/generator/templates/.eslintrc.json index bffb357..6226b63 100644 --- a/lib/generator/templates/.eslintrc.json +++ b/lib/generator/templates/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "next/core-web-vitals" + "extends": [ + "next/core-web-vitals", + "plugin:@tanstack/eslint-plugin-query/recommended" + ] } diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/apollo-client.ts b/lib/generator/templates/src/app/demos/api-all-in-one/apollo-client.ts new file mode 100644 index 0000000..8125ee1 --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/apollo-client.ts @@ -0,0 +1,30 @@ +import { ApolloClient, InMemoryCache, gql } from "@apollo/client"; +import {IBaseRequestOptions} from '@/app/demos/api-all-in-one/fetch-data'; + +export interface IApolloRequestOptions extends IBaseRequestOptions { + apolloConfig?: any; + query?: any; +} + +interface IApolloClientOptions { + uri: any; + cache: any; +} +export const createApolloClient = (options: IApolloClientOptions) => { + return new ApolloClient({ + // uri: "https://countries.trevorblades.com", + // cache: new InMemoryCache(), + uri: options.uri || "https://countries.trevorblades.com", + cache: options.cache || new InMemoryCache(), + }); +}; + +export const queryCountries = gql` + query Countries { + countries { + code + name + emoji + } + } +`; diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/fetch-data.ts b/lib/generator/templates/src/app/demos/api-all-in-one/fetch-data.ts new file mode 100644 index 0000000..f8c9482 --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/fetch-data.ts @@ -0,0 +1,85 @@ +import {createApolloClient, IApolloRequestOptions} from '@/app/demos/api-all-in-one/apollo-client'; +import {fetchData} from '@/app/demos/api-all-in-one/fetch'; +import { ApolloClient, gql } from "@apollo/client"; +import { io, Socket } from 'socket.io-client'; +import {ISocketRequestOptions} from '@/app/demos/api-all-in-one/socket-io'; + +export interface IBaseRequestOptions { + method?: string; + headers?: Record; + body?: Record; +} + +export interface IRequestOptions extends ISocketRequestOptions, IApolloRequestOptions {} + +export const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export class RequestAllInOne { + private readonly headers: Record; + private readonly apolloConfig: any; + private readonly apolloClient: ApolloClient | undefined; // TCacheShape + private readonly socketClient: Socket | undefined; + private options: Partial; + constructor(options: Partial) { + this.options = options || {}; + this.headers = options.headers || defaultHeaders; + + if (options.socketPath) { + this.socketClient = io({ + path: options.socketPath + }); + } + if (options.apolloConfig) { + this.apolloConfig = options.apolloConfig; + this.apolloClient = createApolloClient(this.apolloConfig); + } + } + + async get(url: string) { + return fetchData(url, { headers: this.headers }); + } + + async post(url: string, options: IBaseRequestOptions = {}) { + return fetchData(url, { + method: 'POST', + headers: { + ...this.headers, + ...options.headers, + }, + body: options.body + }); + } + + async gql(url: string, options: IApolloRequestOptions = {}) { + if (!this.apolloClient) { + throw Error('No apollo client found') + } + const { data } = await this.apolloClient.query({ + query: gql`${options.query}`, + }); + + return data; + } + + socket(url: string, options: ISocketRequestOptions) { + let socketClient = this.socketClient; + if (this.options.socketPath !== url || !socketClient) { + socketClient = io({ + path: options.socketPath + }); + } + if (options.type === 'on') { + socketClient.on(options.event, (response: any) => { + console.log(options.event, response); + options.callback && options.callback(response); + }); + } else { + socketClient.emit(options.event, options.value, (response: any) => { + console.log(response); + options.callback && options.callback(response); + }); + } + } +} diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/fetch.ts b/lib/generator/templates/src/app/demos/api-all-in-one/fetch.ts new file mode 100644 index 0000000..570b9bb --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/fetch.ts @@ -0,0 +1,23 @@ +import { IBaseRequestOptions } from '@/app/demos/api-all-in-one/fetch-data'; + +export const fetchData = async (url: string, options: IBaseRequestOptions = {}) => { + + const { method = 'GET', headers = {}, body } = options; + + const fetchOptions: any = { + method, + headers, + }; + + if (body) { + fetchOptions.body = JSON.stringify(body); + } + + const response = await fetch(url, fetchOptions); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/fetch/page.tsx b/lib/generator/templates/src/app/demos/api-all-in-one/fetch/page.tsx new file mode 100644 index 0000000..9b3c5ca --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/fetch/page.tsx @@ -0,0 +1,95 @@ +'use client'; +import {RequestAllInOne} from '@/app/demos/api-all-in-one/fetch-data'; +import {useEffect, useState} from 'react'; + +const client = new RequestAllInOne({}); + +export default function Fetch() { + const [data, setData] = useState(); + + useEffect(() => { + const fetchData = async () => { + const response = await client.get('https://api.github.com/repos/TanStack/query'); + setData(response); + }; + fetchData(); + }, []); + + if (!data) { + return <>No data; + } + + return <> +
+
Fetch Only
+

{data.full_name}

+

{data.description}

+ 👀 {data.subscribers_count}{' '} + ✨ {data.stargazers_count}{' '} + 🍴 {data.forks_count} + {/*
{isFetching ? 'Updating...' : ''}
*/} +
+ ; +} + +// function Example() { +// const { isPending, error, data, isFetching } = useQuery({ +// queryKey: ['repoData'], +// queryFn: async () => { +// const response = await fetch( +// 'https://api.github.com/repos/TanStack/query', +// ) +// return await response.json() +// }, +// }) +// +// if (isPending) return 'Loading...' +// +// if (error) return 'An error has occurred: ' + error.message +// +// return ( +//
+//

{data.full_name}

+//

{data.description}

+// 👀 {data.subscribers_count}{' '} +// ✨ {data.stargazers_count}{' '} +// 🍴 {data.forks_count} +//
{isFetching ? 'Updating...' : ''}
+//
+// ) +// } + +// +// function Todos() { +// // Access the client +// const queryClient = useQueryClient() +// +// // Queries +// const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }) +// +// // Mutations +// const mutation = useMutation({ +// mutationFn: postTodo, +// onSuccess: () => { +// // Invalidate and refetch +// queryClient.invalidateQueries({ queryKey: ['todos'] }) +// }, +// }) +// +// return ( +//
+//
    {query.data?.map((todo) =>
  • {todo.title}
  • )}
+// +// +//
+// ) +// } diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/graphql/page.tsx b/lib/generator/templates/src/app/demos/api-all-in-one/graphql/page.tsx new file mode 100644 index 0000000..7ea6d19 --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/graphql/page.tsx @@ -0,0 +1,40 @@ +'use client'; +import {RequestAllInOne} from '@/app/demos/api-all-in-one/fetch-data'; +import {useEffect, useState} from 'react'; +import {queryCountries} from '@/app/demos/graphql/apollo-client'; +import {InMemoryCache} from '@apollo/client'; + +const client = new RequestAllInOne({ apolloConfig: { + uri: "https://countries.trevorblades.com", + cache: new InMemoryCache(), +}}); + +export default function Fetch() { + const [data, setData] = useState(); + + useEffect(() => { + const fetchData = async () => { + const response = await client.gql('https://countries.trevorblades.com', { + query: queryCountries + }); + const countries = response.countries.slice(0, 4); + setData(countries); + }; + fetchData(); + }, []); + + if (!data) { + return <>No data; + } + + return
+ {data.map((country: any) => ( +
+

{country.name}

+

+ {country.code} - {country.emoji} +

+
+ ))} +
+} diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/layout.tsx b/lib/generator/templates/src/app/demos/api-all-in-one/layout.tsx new file mode 100644 index 0000000..4ef9618 --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/layout.tsx @@ -0,0 +1,21 @@ +import { Menu } from 'antd'; +import Link from 'next/link'; +import type { MenuProps } from 'antd'; + +type MenuItem = Required['items'][number]; + +const items: MenuItem[] = [ + { key: '1', label: Fetch }, + { key: '2', label: Graphql }, + { key: '3', label: Socket }, +]; +export default function APIAllInOneLayout({ + children, // will be a page or nested layout +}: { + children: React.ReactNode +}) { + return <> + +
{children}
+ ; +} diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/page.tsx b/lib/generator/templates/src/app/demos/api-all-in-one/page.tsx new file mode 100644 index 0000000..e788ced --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/page.tsx @@ -0,0 +1,83 @@ +'use client'; +// useQuery use client only +// Todo: server side integration. +import { + useQuery, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; + +// Create a client +const queryClient = new QueryClient() + +export default function APIAllInOne() { + return <> + +

api all in one.

+ {/**/} + +
+ ; +} + +function Example() { + const { isPending, error, data, isFetching } = useQuery({ + queryKey: ['repoData'], + queryFn: async () => { + const response = await fetch( + 'https://api.github.com/repos/TanStack/query', + ) + return await response.json() + }, + }) + + if (isPending) return 'Loading...' + + if (error) return 'An error has occurred: ' + error.message + + return ( +
+

{data.full_name}

+

{data.description}

+ 👀 {data.subscribers_count}{' '} + ✨ {data.stargazers_count}{' '} + 🍴 {data.forks_count} +
{isFetching ? 'Updating...' : ''}
+
+ ) +} + +// +// function Todos() { +// // Access the client +// const queryClient = useQueryClient() +// +// // Queries +// const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }) +// +// // Mutations +// const mutation = useMutation({ +// mutationFn: postTodo, +// onSuccess: () => { +// // Invalidate and refetch +// queryClient.invalidateQueries({ queryKey: ['todos'] }) +// }, +// }) +// +// return ( +//
+//
    {query.data?.map((todo) =>
  • {todo.title}
  • )}
+// +// +//
+// ) +// } diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/socket-io.ts b/lib/generator/templates/src/app/demos/api-all-in-one/socket-io.ts new file mode 100644 index 0000000..9548403 --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/socket-io.ts @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { io } from 'socket.io-client'; +import { IBaseRequestOptions } from '@/app/demos/api-all-in-one/fetch-data'; + +export interface ISocketRequestOptions extends IBaseRequestOptions { + type: 'on' | 'emit' + event: string + value?: string + callback?: Function; + queryKey: string[] + socketPath?: string; +} + +// TODO +export const useWebSocket = (url: string, options: ISocketRequestOptions) => { + const queryClient = useQueryClient(); + + useEffect(() => { + const _socket = io({ + path: url // '/socket-io/' + }); + + _socket.emit(options.event, options.value, (response: any) => { + console.log(response.status); + console.log(response.text); + }); + + _socket.on(options.event, (socketId: string, message: string) => { + console.log(options.event, socketId, message); + const data = JSON.parse(message); + + queryClient.setQueryData(options.queryKey, (oldData: []) => { + // 假设新数据是旧数据的更新,可以根据实际需求调整逻辑 + return [...(oldData || []), data]; + }); + }); + + return () => { + _socket.close(); + }; + }, [url, queryClient, options.queryKey, options.event, options.value]); +}; diff --git a/lib/generator/templates/src/app/demos/api-all-in-one/socket/page.tsx b/lib/generator/templates/src/app/demos/api-all-in-one/socket/page.tsx new file mode 100644 index 0000000..ff7007d --- /dev/null +++ b/lib/generator/templates/src/app/demos/api-all-in-one/socket/page.tsx @@ -0,0 +1,73 @@ +'use client'; +import { Button } from 'antd' +import {useEffect, useState} from 'react'; +import {RequestAllInOne} from '@/app/demos/api-all-in-one/fetch-data'; + +export default function SocketPage() { + const [client, setClient] = useState(); + const [message, setMessage] = useState(''); + + useEffect(() => { + if (!client) { + return; + } + client.socket('/socket-io/', { + type: 'on', + event: 'answer', + callback: (response: any) => { + console.log('answer: ', response); + setMessage(response); + }, + queryKey: ['answer'] + }); + }, [client]); + + return <> +

Socket io demo.

+ + + +
socket message:
+
{message}
+ +} diff --git a/lib/generator/templates/src/app/demos/socket/page.tsx b/lib/generator/templates/src/app/demos/socket/page.tsx index 22134b7..ee0c375 100644 --- a/lib/generator/templates/src/app/demos/socket/page.tsx +++ b/lib/generator/templates/src/app/demos/socket/page.tsx @@ -2,7 +2,6 @@ import { io } from 'socket.io-client'; import { Button } from 'antd' import {useEffect, useState} from 'react'; -import {response} from 'express'; export default function SocketPage() { const [socket, setSocket] = useState(); @@ -12,8 +11,8 @@ export default function SocketPage() { if (!socket) { return; } - socket.on('answer-chain-gpt', (socketId: string, message: string) => { - console.log('answer-chain-gpt: ', socketId, message); + socket.on('answer', (message: string) => { + console.log('answer: ', message); setAnswer(message); }); }, [socket]); @@ -27,8 +26,9 @@ export default function SocketPage() { console.log('_socket: ', _socket); setSocket(_socket); _socket.on("connect", () => { - console.log(_socket); // x8WIv7-mJelg7on_ALbx + console.log(_socket); console.log('connect !!!'); + setAnswer('connect !!!'); }); }}>Connect + socket.emit("hello", "hzz780"); + }}>Trigger event answer
answer:
{answer}
diff --git a/lib/generator/templates/src/config/demo/configMenu.ts b/lib/generator/templates/src/config/demo/configMenu.ts index 31a9926..dcc5206 100644 --- a/lib/generator/templates/src/config/demo/configMenu.ts +++ b/lib/generator/templates/src/config/demo/configMenu.ts @@ -57,4 +57,12 @@ export const menuList: IMenuItemData[] = [ label: 'socket', href: '/demos/socket', }, + { + label: 'Graphql', + href: '/demos/graphql/server', + }, + { + label: 'API All in one', + href: '/demos/api-all-in-one', + }, ];