= ({
15 | label,
16 | labelDescription,
17 | name,
18 | width = '100%',
19 | height = 200,
20 | placeholder,
21 | register,
22 | watch,
23 | defaultValue = null,
24 | validationErrors,
25 | className = '',
26 | }) => {
27 | const imageUrl = watch(name)
28 |
29 | const [currentImageIsValid, currentImageIsValidSet] = useState(true)
30 |
31 | return (
32 |
33 |
34 |
35 |
46 |
47 |
48 |
{
59 | currentImageIsValidSet(false)
60 | }}
61 | onLoad={() => {
62 | currentImageIsValidSet(true)
63 | }}
64 | />
65 |
71 |
72 |
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/src/pages/docs/index.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from '../../components/Layout/Layout'
2 | import { LinksList } from '../../model/site/LinksList'
3 | import dayjs from 'dayjs'
4 | import { getAllDocs, MetaTypes } from '../../utils/docs'
5 | import { GetStaticProps } from 'next'
6 | import Link from 'next/link'
7 |
8 | export const getStaticProps: GetStaticProps = async () => {
9 | const docs = getAllDocs()
10 |
11 | return {
12 | props: { docs },
13 | }
14 | }
15 |
16 | type DocsProps = {
17 | docs: MetaTypes[]
18 | }
19 |
20 | export default function Docs({ docs }: DocsProps) {
21 | return (
22 |
25 | Documentation
26 | {process.env.NEXT_PUBLIC_SITE_NAME}
27 |
28 | }
29 | menuItems={Object.values(LinksList)}
30 | >
31 |
32 | Documentation
33 |
34 |
35 | {docs?.length > 0 &&
36 | docs.map((item) => (
37 |
38 |
39 |
47 |
48 |
49 |
50 |
51 | {item.meta.date &&
{dayjs(item.meta.date).format('YYYY-MM-DD')}
}
52 |
57 |
58 |
59 |
60 |
61 | ))}
62 |
63 |
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils/typedFetch/typedFetch.ts:
--------------------------------------------------------------------------------
1 | import queryString from 'query-string'
2 | import { HTTP_METHODS } from './HTTP_METHODS'
3 | import { RESPONSE_TYPE } from './RESPONSE_TYPE'
4 |
5 | export type TypedFetchResult = {
6 | status: number
7 | statusText: string
8 | error?: any
9 | outputData?: P
10 | }
11 |
12 | export type TypedFetchParams = {
13 | url: string
14 | method?: HTTP_METHODS
15 | inputData?: INPUT_TYPE
16 | headers?: Headers
17 | responseType?: RESPONSE_TYPE
18 | }
19 |
20 | export default async function typedFetch({
21 | url,
22 | method = 'get',
23 | responseType = 'json',
24 | headers = new Headers({
25 | Accept: 'application/json',
26 | 'Content-Type': 'application/json',
27 | }),
28 | inputData: data,
29 | }: TypedFetchParams): Promise> {
30 | const fetchOptions: {
31 | headers: Headers
32 | method: string
33 | body?: string
34 | } = {
35 | headers,
36 | method,
37 | }
38 |
39 | let qs = ''
40 | if (method === 'post' || method === 'put') {
41 | // TODO: Add a way to send normal post, without json
42 | fetchOptions.body = JSON.stringify(data)
43 | } else {
44 | qs = `?${queryString.stringify(data || {})}`
45 | }
46 |
47 | const res = await fetch(`${url}${qs}`, fetchOptions)
48 |
49 | // NOT OK!
50 | if (!res.ok || (res.status < 200 && res.status > 299)) {
51 | let errorJSON = {}
52 | try {
53 | // try get JSON
54 | errorJSON = await res.json()
55 | } catch (error) {
56 | console.error('> res.json() error: ', error)
57 | }
58 |
59 | return {
60 | status: res.status,
61 | statusText: res.statusText,
62 | error: errorJSON,
63 | outputData: undefined,
64 | }
65 | }
66 |
67 | // try get JSON
68 | let resultJSON: OUTPUT_TYPE
69 | if (responseType === 'json') {
70 | try {
71 | resultJSON = await res.json()
72 | // OK!
73 | return {
74 | status: res.status,
75 | statusText: res.statusText,
76 | error: null,
77 | outputData: resultJSON,
78 | }
79 | } catch (error) {
80 | console.error('>> await res.json() error: ', error)
81 | return {
82 | status: res.status,
83 | statusText: res.statusText,
84 | error,
85 | outputData: undefined,
86 | }
87 | }
88 | }
89 |
90 | // ?
91 | return {
92 | status: res.status,
93 | statusText: res.statusText,
94 | error: '? typed',
95 | outputData: undefined,
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { addDecorator } = require('@storybook/react')
3 |
4 | const toPath = (_path) => path.join(process.cwd(), _path)
5 |
6 | module.exports = {
7 | stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
8 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
9 |
10 | webpackFinal: async (config, { configType }) => {
11 | // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
12 | // You can change the configuration based on that.
13 | // 'PRODUCTION' is used when building the static version of storybook.
14 |
15 | // Added to support PostCSS v8.X
16 | /**
17 | * CSS handling, specifically overriding postcss loader
18 | */
19 | // Find the only Storybook webpack rule that tests for css
20 | const cssRule = config.module.rules.find((rule) => 'test.css'.match(rule.test))
21 | // Which loader in this rule mentions the custom Storybook postcss-loader?
22 | const loaderIndex = cssRule.use.findIndex((loader) => {
23 | // Loaders can be strings or objects
24 | const loaderString = typeof loader === 'string' ? loader : loader.loader
25 | // Find the first mention of "postcss-loader", it may be in a string like:
26 | // "@storybook/core/node_modules/postcss-loader"
27 | return loaderString.includes('postcss-loader')
28 | })
29 | // Simple loader string form, removes the obsolete "options" key
30 | cssRule.use[loaderIndex] = 'postcss-loader'
31 |
32 | // ignore *.po files
33 | config.module.rules.push({
34 | test: /\.(po)$/,
35 | use: [
36 | {
37 | loader: require.resolve('ignore-loader'),
38 | },
39 | ],
40 | })
41 |
42 | // SVG
43 | // Needed for SVG importing using svgr
44 | const indexOfRuleToRemove = config.module.rules.findIndex((rule) =>
45 | rule.test?.toString().includes('svg')
46 | )
47 |
48 | config.module.rules.splice(indexOfRuleToRemove, 1, {
49 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
50 | loader: require.resolve('file-loader'),
51 | options: {
52 | name: 'static/media/[name].[hash:8].[ext]',
53 | esModule: false,
54 | },
55 | })
56 | config.module.rules.push({
57 | test: /\.svg$/,
58 | use: [
59 | {
60 | loader: '@svgr/webpack',
61 | options: {
62 | svgo: false,
63 | },
64 | },
65 | ],
66 | })
67 |
68 | return {
69 | ...config,
70 | resolve: {
71 | ...config.resolve,
72 | alias: {
73 | ...config.resolve.alias,
74 | },
75 | },
76 | }
77 | },
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/forms/FormLocalImage/FormLocalImage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Story, Meta } from '@storybook/react/types-6-0'
3 | import { FormLocalImage, FormLocalImageProps } from './FormLocalImage'
4 |
5 | export default {
6 | title: 'Component/Forms/FormLocalImage',
7 | component: FormLocalImage,
8 | argTypes: {
9 | register: {
10 | table: {
11 | disable: true,
12 | },
13 | },
14 | validationErrors: {
15 | table: {
16 | disable: true,
17 | },
18 | },
19 | },
20 | } as Meta
21 |
22 | const Template: Story = (args) =>
23 |
24 | export const Image_Clean = Template.bind({})
25 | Image_Clean.args = {
26 | name: 'image_name',
27 | register: () => {
28 | /* noop */
29 | },
30 | validationErrors: {},
31 | }
32 |
33 | export const Image_Height = Template.bind({})
34 | Image_Height.args = {
35 | name: 'image_name',
36 | register: () => {
37 | /* noop */
38 | },
39 | validationErrors: {},
40 | height: 400,
41 | }
42 |
43 | export const Image_Small = Template.bind({})
44 | Image_Small.args = {
45 | name: 'image_name',
46 | register: () => {
47 | /* noop */
48 | },
49 | validationErrors: {},
50 | width: 100,
51 | height: 100,
52 | }
53 |
54 | export const Image_DefaultValue = Template.bind({})
55 | Image_DefaultValue.args = {
56 | name: 'image_name',
57 | defaultValue: 'https://via.placeholder.com/1080x1920.png?text=Image+Placeholder',
58 | register: () => {
59 | /* noop */
60 | },
61 |
62 | validationErrors: {},
63 | }
64 |
65 | export const Image_Placeholder_changes_Button_Label = Template.bind({})
66 | Image_Placeholder_changes_Button_Label.args = {
67 | name: 'image_name',
68 | placeholder: 'Select an Image',
69 | defaultValue: 'https://via.placeholder.com/1080x1920.png?text=Image+Placeholder',
70 | register: () => {
71 | /* noop */
72 | },
73 |
74 | validationErrors: {},
75 | }
76 |
77 | export const Image_Error = Template.bind({})
78 | Image_Error.args = {
79 | name: 'text_with_error',
80 | placeholder: 'Select an Image',
81 | register: () => {
82 | /* noop */
83 | },
84 | validationErrors: {
85 | text_with_error: {
86 | message: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
87 | },
88 | },
89 | }
90 |
91 | export const Image_CustomClassName_add_border = Template.bind({})
92 | Image_CustomClassName_add_border.args = {
93 | label: 'Label:',
94 | name: 'image_name',
95 | placeholder: 'Select an Image',
96 | register: () => {
97 | /* noop */
98 | },
99 | validationErrors: {},
100 | className: 'border-4',
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/forms/FormLocalImage/FormLocalImage.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as TestingLib from '@testing-library/react'
3 | import '@testing-library/jest-dom/extend-expect'
4 | import { FormLocalImage } from './FormLocalImage'
5 | import { fireEvent } from '@testing-library/react'
6 |
7 | describe('Form Local Image Component', () => {
8 | it('should render a component', async () => {
9 | const render = TestingLib.render(
10 | {
15 | /* noop */
16 | }}
17 | validationErrors={undefined}
18 | className='text-lg select-accent'
19 | />
20 | )
21 | expect(render.getByTitle('Select an Image')).toBeInTheDocument()
22 | })
23 |
24 | it('should render component with default value', async () => {
25 | const render = TestingLib.render(
26 | {
31 | /* noop */
32 | }}
33 | validationErrors={undefined}
34 | />
35 | )
36 |
37 | const allOptions = render.getByRole('img') as HTMLImageElement
38 | expect(allOptions).toHaveAttribute('src')
39 | expect(allOptions.src).toEqual(
40 | 'https://via.placeholder.com/1080x1920.png?text=Image+Placeholder'
41 | )
42 | })
43 |
44 | it('should click and change the selected image', async () => {
45 | const render = TestingLib.render(
46 | {
51 | /* noop */
52 | }}
53 | validationErrors={undefined}
54 | />
55 | )
56 |
57 | fireEvent.click(render.getByTitle('Select an Image'), {
58 | target: { src: 'https://via.placeholder.com/1080x1920.png?text=Image+Placeholder' },
59 | })
60 |
61 | const select = render.getByTitle('Select an Image') as HTMLImageElement
62 | expect(select.src).toEqual('https://via.placeholder.com/1080x1920.png?text=Image+Placeholder')
63 | })
64 |
65 | it('should render component with error message', async () => {
66 | const render = TestingLib.render(
67 | {
72 | /* noop */
73 | }}
74 | validationErrors={{
75 | text_with_error: {
76 | message: 'Error Message',
77 | },
78 | }}
79 | />
80 | )
81 | expect(render.getByText('Error Message')).toHaveClass('text-error')
82 | expect(render.getByTitle('Select an Image')).toHaveClass('text-error')
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/src/components/Layout/Layout.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Story, Meta } from '@storybook/react/types-6-0'
3 | import { Layout, LayoutProps } from './Layout'
4 | import { LinksList } from '../../model/site/LinksList'
5 | import { UserProvider } from '@auth0/nextjs-auth0'
6 |
7 | export default {
8 | title: 'Component/Pages/Layout',
9 | component: Layout,
10 | argTypes: {},
11 | } as Meta
12 |
13 | const Template: Story = (args) => (
14 |
15 |
16 |
17 | )
18 |
19 | // Default scenario
20 | export const MainLayout = Template.bind({})
21 | MainLayout.args = {
22 | title: (
23 |
24 |
HOME
25 |
{process.env.NEXT_PUBLIC_SITE_NAME}
26 |
27 | ),
28 | menuItems: Object.values(LinksList),
29 | children:
30 | 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at enim congue scelerisque. Sed suscipit metu non iaculis semper consectetur adipiscing elit.',
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/DropDown/DropDown.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useEffect, useState } from 'react'
2 | import { Transition, Menu } from '@headlessui/react'
3 | import classnames from 'classnames'
4 | import { FaCaretDown } from 'react-icons/fa'
5 |
6 | export interface DropDownProps {
7 | label: ReactNode
8 | items: { id: string; value: ReactNode }[]
9 | onSelect: (string) => void
10 | selectedId: string | null
11 | className?: string
12 | classNameButton?: string
13 | width?: number
14 | }
15 |
16 | export const DropDown: React.FC = ({
17 | label,
18 | items,
19 | onSelect,
20 | selectedId,
21 | className,
22 | classNameButton,
23 | width = 176,
24 | }) => {
25 | const [currentValue, currentValueSet] = useState(null)
26 | useEffect(() => {
27 | const localValue = items.find((i) => i.id === selectedId)
28 | if (localValue) {
29 | currentValueSet(String(localValue.value))
30 | } else {
31 | currentValueSet('')
32 | }
33 | }, [items, selectedId])
34 |
35 | if (currentValue === null) {
36 | return null
37 | }
38 |
39 | return (
40 |
41 | {({ open }) => (
42 |
43 |
44 | {currentValue === '' ? (
45 |
46 |
{label}
47 |
{' '}
48 |
49 | ) : (
50 | {currentValue}
51 | )}
52 |
53 |
62 |
67 | {Object.values(items).map((t) => (
68 |
69 |
70 | {
76 | onSelect(t.id)
77 | }}
78 | >
79 | {t.value}
80 |
81 |
82 |
83 | ))}
84 |
85 |
86 |
87 | )}
88 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/forms/FormImage/FormImage.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as TestingLib from '@testing-library/react'
3 | import '@testing-library/jest-dom/extend-expect'
4 | import { FormImage } from './FormImage'
5 | import { fireEvent } from '@testing-library/react'
6 |
7 | describe('Form Local Image Component', () => {
8 | it('should render a component', async () => {
9 | const render = TestingLib.render(
10 | {
15 | /* noop */
16 | }}
17 | watch={() => {
18 | /* noop */
19 | }}
20 | validationErrors={undefined}
21 | className='text-lg select-accent'
22 | />
23 | )
24 | expect(render.getByText('Name')).toBeInTheDocument()
25 | })
26 |
27 | it('should render component with default value', async () => {
28 | const render = TestingLib.render(
29 | {
34 | /* noop */
35 | }}
36 | watch={() => {
37 | /* noop */
38 | }}
39 | validationErrors={undefined}
40 | />
41 | )
42 |
43 | const allOptions = render.getByRole('img') as HTMLImageElement
44 | expect(allOptions).toHaveAttribute('src')
45 | expect(allOptions.src).toEqual(
46 | 'https://via.placeholder.com/1080x1920.png?text=Image+Placeholder'
47 | )
48 | })
49 |
50 | it('should click and change the selected image', async () => {
51 | const render = TestingLib.render(
52 | {
57 | /* noop */
58 | }}
59 | watch={() => {
60 | /* noop */
61 | }}
62 | validationErrors={undefined}
63 | />
64 | )
65 |
66 | fireEvent.change(render.getByRole('img'), {
67 | target: { src: 'https://via.placeholder.com/1080x1920.png?text=Image+Placeholder' },
68 | })
69 |
70 | const select = render.getByRole('img') as HTMLImageElement
71 | expect(select.src).toEqual('https://via.placeholder.com/1080x1920.png?text=Image+Placeholder')
72 | })
73 |
74 | it('should render component with error message', async () => {
75 | const render = TestingLib.render(
76 | {
81 | /* noop */
82 | }}
83 | watch={() => {
84 | /* noop */
85 | }}
86 | validationErrors={{
87 | text_with_error: {
88 | message: 'Error Message',
89 | },
90 | }}
91 | />
92 | )
93 |
94 | expect(render.getByText('Error Message')).toHaveClass('text-error')
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/src/components/Cards/Cards.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import React, { ReactNode } from 'react'
3 |
4 | export interface CardsProps {
5 | title?: string
6 | text?: string
7 | url?: string | ReactNode
8 | urlText?: string
9 | }
10 |
11 | export const Cards: React.FC = () => {
12 | const data = [
13 | {
14 | title: 'Storybook',
15 | text: 'this project works with storybook using TailwindCSS and PostCSS',
16 | url: 'https://main--60d0b5d829870700396e0a3b.chromatic.com',
17 | urlText: 'See storybook',
18 | },
19 | {
20 | title: 'Code Generator',
21 | text: 'Generate code directly from this project if you are running on http://localhost:3000',
22 | url: '/code-generator',
23 | urlText: 'Generate code',
24 | },
25 | {
26 | title: 'CRUD Example',
27 | text: 'A standard way to List, Set, Update and Delete items',
28 | url: '/list_items?page=1',
29 | urlText: 'List items',
30 | },
31 | {
32 | title: 'TypedFetch',
33 | text: 'We created a standard typed-fetch using typeScript to manage API calls. That way you will be typing input and output of all fetch calls',
34 | url: '/typed-fetch-examples',
35 | urlText: 'Test fetch',
36 | },
37 | {
38 | title: 'Authentication',
39 | text: 'We use a simple integration with auth0. The incredible integration is made with the nextjs-auth0',
40 | url: '/typed-fetch-examples',
41 | urlText: 'Login Now',
42 | },
43 | {
44 | title: 'Universal Validation',
45 | text: 'ZOD validator is a simple validation library. We use it to validate input fields on server side and on cliente side',
46 | url: '/list_items/cb0f6982-de0a-4b35-804f-5ed5e8d7bed3',
47 | urlText: 'Check Validation',
48 | },
49 | {
50 | title: 'Users',
51 | text: 'You can see and edit users, only if you are an administrator.',
52 | url: '/users',
53 | urlText: 'See users',
54 | },
55 | ]
56 | return (
57 |
58 | {data?.map((cards, i) => (
59 |
60 |
61 | <>
62 |
{cards.title}
63 |
{cards.text}
64 |
84 | >
85 |
86 |
87 | ))}
88 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/src/pages/api/auth/[...auth0].ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { handleAuth, handleCallback, handleLogin, handleLogout } from '@auth0/nextjs-auth0'
3 | import { NextApiRequest, NextApiResponse } from 'next'
4 | import { Roles_Enum, Users_Update_Column } from '../../../graphql/generated'
5 | import { Insert_users_one_api_post } from '../../../model/api-models/users/Insert_users_one_api_post'
6 | import { Users_by_pk_api_get } from '../../../model/api-models/users/Users_by_pk_api_get'
7 | import GqlSdkHelper from '../../../utils/GqlSdkHelper'
8 |
9 | // from: https://github.com/auth0/nextjs-auth0/issues/108#issuecomment-800059278
10 |
11 | const audience = process.env.AUTH0_AUDIENCE
12 | const scope = process.env.AUTH0_SCOPE
13 |
14 | function getUrls({ req }) {
15 | const { host } = req.headers
16 | const protocol = process.env.VERCEL_URL ? 'https' : 'http'
17 | const redirectUri = `${protocol}://${host}/api/auth/callback`
18 | const returnTo = `${protocol}://${host}`
19 | return {
20 | redirectUri,
21 | returnTo,
22 | }
23 | }
24 |
25 | export default handleAuth({
26 | async callback(req: NextApiRequest, res: NextApiResponse) {
27 | try {
28 | const { redirectUri } = getUrls({ req })
29 | await handleCallback(req, res, {
30 | redirectUri,
31 | afterCallback: async (_req, _res, session /* , state */) => {
32 | const userResultObj: Users_by_pk_api_get['output'] = await new GqlSdkHelper()
33 | .getSdk()
34 | .users_by_pk({
35 | id: session.user.sub,
36 | })
37 |
38 | // get user ROLE
39 | // check if users exists and get current role
40 | if (userResultObj?.users_by_pk?.role) {
41 | // return existing user
42 | session.user.role = userResultObj?.users_by_pk?.role as Roles_Enum
43 | return session
44 | }
45 | // save new user
46 | const inputData = {
47 | id: session.user.sub,
48 | name: session.user.name,
49 | email: session.user.email,
50 | image: session.user.picture,
51 | role: Roles_Enum.User,
52 | } as Insert_users_one_api_post['input']
53 | const insertResultObj: Insert_users_one_api_post['output'] = await new GqlSdkHelper()
54 | .getSdk()
55 | .insert_users_one({
56 | object: inputData,
57 | update_columns: Object.values(Users_Update_Column),
58 | })
59 |
60 | session.user.role = insertResultObj.insert_users_one?.role
61 | return session
62 | },
63 | })
64 | } catch (error) {
65 | res.status(error.status || 500).end(error.message)
66 | }
67 | },
68 |
69 | async login(req: NextApiRequest, res: NextApiResponse) {
70 | try {
71 | const { redirectUri, returnTo } = getUrls({ req })
72 |
73 | await handleLogin(req, res, {
74 | authorizationParams: {
75 | audience,
76 | scope,
77 | redirectUri,
78 | },
79 | returnTo,
80 | })
81 | } catch (error) {
82 | res.status(error.status || 400).end(error.message)
83 | }
84 | },
85 |
86 | async logout(req: NextApiRequest, res: NextApiResponse) {
87 | const { returnTo } = getUrls({ req })
88 | await handleLogout(req, res, {
89 | returnTo,
90 | })
91 | },
92 | })
93 |
--------------------------------------------------------------------------------
/src/components/forms/FormInputColor/FormInputColor_Form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { useForm } from 'react-hook-form'
3 | import { zodResolver } from '@hookform/resolvers/zod'
4 | import withReactContent from 'sweetalert2-react-content'
5 | import Swal from 'sweetalert2'
6 | import { FormInputColor, FormInputColorProps } from './FormInputColor'
7 | import * as z from 'zod'
8 | import { CodeBlock } from '../../CodeBlock/CodeBlock'
9 | import isHexColor from 'validator/lib/isHexColor'
10 |
11 | export const FormInputColor_Form: React.FC = (formProps) => {
12 | const {
13 | handleSubmit,
14 | register,
15 | formState: { errors: validationErrors },
16 | watch,
17 | setValue,
18 | } = useForm({
19 | mode: 'onChange',
20 | resolver: zodResolver(
21 | z.object({
22 | color_input: z
23 | .string()
24 | .min(7)
25 | .refine((value) => isHexColor(value), {
26 | message: 'invalid color',
27 | }),
28 | })
29 | ),
30 | })
31 |
32 | const onSubmit = handleSubmit(
33 | async (submitProps) => {
34 | // const resultJSON = await fetchResponse.json()
35 | const myAlert = withReactContent(Swal)
36 | await myAlert.fire({
37 | title: 'submited',
38 | html: ,
39 | confirmButtonText: 'close',
40 | })
41 | },
42 | (submitErrors) => {
43 | console.error('-- submitErrors: ', submitErrors)
44 | }
45 | )
46 |
47 | return (
48 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames'
2 | import React, { useCallback, useEffect, useState } from 'react'
3 |
4 | export interface PaginationProps {
5 | totalPages: number
6 | currentPage: number
7 | className?: string
8 | previousButtonTitle?: string
9 | nextButtonTitle?: string
10 | onPageSet: (number) => void
11 | }
12 |
13 | export const Pagination: React.FC = ({
14 | className = '',
15 | totalPages = 1,
16 | currentPage = 1,
17 | onPageSet,
18 | previousButtonTitle = 'Previous',
19 | nextButtonTitle = 'Next',
20 | }) => {
21 | const [listNumberOfPages, listNumberOfPagesSet] = useState([])
22 |
23 | const handlePagination = useCallback(() => {
24 | let startOfPageOnList = 0
25 | let endOfPageOnList = 0
26 |
27 | if (currentPage === 1) {
28 | startOfPageOnList = currentPage
29 | } else if (currentPage === 2) {
30 | startOfPageOnList = currentPage - 1
31 | } else if (currentPage >= totalPages) {
32 | if (totalPages > 2) {
33 | startOfPageOnList = totalPages - 1
34 | } else if (totalPages === 1) {
35 | startOfPageOnList = totalPages
36 | }
37 | } else {
38 | startOfPageOnList = currentPage - 2
39 | }
40 |
41 | if (totalPages <= currentPage) {
42 | endOfPageOnList = totalPages
43 | } else if (totalPages === currentPage + 1) {
44 | endOfPageOnList = currentPage + 1
45 | } else {
46 | endOfPageOnList = currentPage + 2
47 | }
48 | const listOfNumbers: number[] = []
49 | for (let i = startOfPageOnList; i <= endOfPageOnList; i += 1) {
50 | listOfNumbers.push(i)
51 | }
52 |
53 | listNumberOfPagesSet(listOfNumbers)
54 | }, [currentPage, totalPages])
55 |
56 | const handlePreviousPage = () => {
57 | if (currentPage > 1) {
58 | onPageSet(currentPage - 1)
59 | }
60 | }
61 |
62 | const handleNextPage = () => {
63 | if (currentPage < totalPages) {
64 | onPageSet(currentPage + 1)
65 | }
66 | }
67 |
68 | const handlePage = (page) => {
69 | onPageSet(page)
70 | }
71 |
72 | useEffect(() => {
73 | handlePagination()
74 | }, [currentPage, handlePagination])
75 | return (
76 |
77 | {
80 | handlePreviousPage()
81 | }}
82 | className={classnames(`btn ${className}`, {
83 | 'btn-disabled cursor-not-allowed': currentPage === 1,
84 | })}
85 | >
86 | {previousButtonTitle}
87 |
88 | {listNumberOfPages.map((item) => (
89 | {
93 | handlePage(item)
94 | }}
95 | className={classnames(`btn ${className}`, {
96 | 'btn-active': item === currentPage,
97 | })}
98 | >
99 | {item}
100 |
101 | ))}
102 | {
105 | handleNextPage()
106 | }}
107 | className={classnames(`btn ${className}`, {
108 | 'btn-disabled cursor-not-allowed': totalPages === currentPage,
109 | })}
110 | >
111 | {nextButtonTitle}
112 |
113 |
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/forms/FormSelect/FormSelect.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { OptionHTMLAttributes } from 'react'
2 | import * as TestingLib from '@testing-library/react'
3 | import '@testing-library/jest-dom/extend-expect'
4 | import { EMPTY_SELECT_OPTION_VALUE, FormSelect } from './FormSelect'
5 | import { fireEvent } from '@testing-library/react'
6 |
7 | describe('Select Component', () => {
8 | it('should render a component', async () => {
9 | const render = TestingLib.render(
10 | {
15 | /* noop */
16 | }}
17 | validationErrors={undefined}
18 | className='text-lg select-accent'
19 | options={[
20 | { value: 'it1', label: 'item 1' },
21 | { value: 'it2', label: 'item 2' },
22 | ]}
23 | />
24 | )
25 | expect(render.getByText('Select an Item')).toBeInTheDocument()
26 | })
27 |
28 | it('should render component with default value', async () => {
29 | const render = TestingLib.render(
30 | {
34 | /* noop */
35 | }}
36 | validationErrors={undefined}
37 | defaultValue='it2'
38 | className='text-lg select-accent'
39 | options={[
40 | { value: 'it1', label: 'item 1' },
41 | { value: 'it2', label: 'item 2' },
42 | ]}
43 | />
44 | )
45 |
46 | const allOptions = render.getAllByRole('option') as OptionHTMLAttributes
47 |
48 | expect(allOptions[0].value).toEqual(EMPTY_SELECT_OPTION_VALUE)
49 | expect(allOptions[1].value).toEqual('it1')
50 | expect(allOptions[2].value).toEqual('it2')
51 | })
52 |
53 | it('should click and change the selected value', async () => {
54 | const render = TestingLib.render(
55 | {
60 | /* noop */
61 | }}
62 | validationErrors={undefined}
63 | defaultValue='it2'
64 | className='text-lg select-accent'
65 | options={[
66 | { value: 'it1', label: 'item 1' },
67 | { value: 'it2', label: 'item 2' },
68 | ]}
69 | />
70 | )
71 |
72 | fireEvent.click(render.getByTestId('select_name'), { target: { value: 'it2' } })
73 |
74 | const select = render.getByTestId('select_name') as HTMLSelectElement
75 | expect(select.value).toEqual('it2')
76 | })
77 |
78 | it('should render component with error message', async () => {
79 | const render = TestingLib.render(
80 | {
85 | /* noop */
86 | }}
87 | validationErrors={{
88 | text_with_error: {
89 | message: 'Error Message',
90 | },
91 | }}
92 | defaultValue='it2'
93 | className='text-lg select-accent'
94 | options={[
95 | { value: 'it1', label: 'item 1' },
96 | { value: 'it2', label: 'item 2' },
97 | ]}
98 | />
99 | )
100 | expect(render.getByText('Error Message')).toHaveClass('text-error')
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/.env.production.sh.example:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # set -eux
3 |
4 | # heroku config:set HASURA_GRAPHQL_ADMIN_SECRET=91823712yue891hd18hd128eh1298 -a nextjs-opinionated-hasura
5 |
6 | replace_vercel_plain () {
7 | # log
8 | printf "\n$1 - $2\n"
9 |
10 | # remove env
11 | vercel env rm $2 $1 -y
12 |
13 | # add env
14 | printf "$3" | vercel env add $2 $1
15 | }
16 |
17 | replace_vercel_plain preview NEXT_PUBLIC_GA_ID 'xxx'
18 | replace_vercel_plain production NEXT_PUBLIC_GA_ID 'xxx'
19 |
20 | replace_vercel_plain preview NEXT_PUBLIC_SITE_NAME 'xxx'
21 | replace_vercel_plain production NEXT_PUBLIC_SITE_NAME 'xxx'
22 |
23 | replace_vercel_plain preview NEXT_PUBLIC_SITE_URL 'xxx'
24 | replace_vercel_plain production NEXT_PUBLIC_SITE_URL 'xxx'
25 |
26 | replace_vercel_plain preview NEXT_PUBLIC_SITE_IMAGE 'xxx'
27 | replace_vercel_plain production NEXT_PUBLIC_SITE_IMAGE 'xxx'
28 |
29 | replace_vercel_plain preview NEXT_PUBLIC_SITE_DESCRIPTION 'xxx'
30 | replace_vercel_plain production NEXT_PUBLIC_SITE_DESCRIPTION 'xxx'
31 |
32 | replace_vercel_plain preview NEXT_PUBLIC_SITE_KEYWORDS 'xxx'
33 | replace_vercel_plain production NEXT_PUBLIC_SITE_KEYWORDS 'xxx'
34 |
35 |
36 | replace_vercel_plain preview AUTH0_SECRET 'xxx'
37 | replace_vercel_plain production AUTH0_SECRET 'xxx'
38 |
39 | replace_vercel_plain preview AUTH0_BASE_URL 'xxx'
40 | replace_vercel_plain production AUTH0_BASE_URL 'xxx'
41 |
42 | replace_vercel_plain preview AUTH0_ISSUER_BASE_URL 'xxx'
43 | replace_vercel_plain production AUTH0_ISSUER_BASE_URL 'xxx'
44 |
45 | replace_vercel_plain preview AUTH0_CLIENT_ID 'xxx'
46 | replace_vercel_plain production AUTH0_CLIENT_ID 'xxx'
47 |
48 | replace_vercel_plain preview AUTH0_CLIENT_SECRET 'xxx'
49 | replace_vercel_plain production AUTH0_CLIENT_SECRET 'xxx'
50 |
51 | replace_vercel_plain preview AUTH0_SCOPE 'xxx'
52 | replace_vercel_plain production AUTH0_SCOPE 'xxx'
53 |
54 | replace_vercel_plain preview AUTH0_AUDIENCE 'xxx'
55 | replace_vercel_plain production AUTH0_AUDIENCE 'xxx'
56 |
57 |
58 | replace_vercel_plain preview NEXT_PUBLIC_SENTRY_DSN 'xxx'
59 | replace_vercel_plain production NEXT_PUBLIC_SENTRY_DSN 'xxx'
60 |
61 | replace_vercel_plain preview SENTRY_SERVER_INIT_PATH 'xxx'
62 | replace_vercel_plain production SENTRY_SERVER_INIT_PATH 'xxx'
63 |
64 | replace_vercel_plain preview NEXT_PUBLIC_SENTRY_DSN 'xxx'
65 | replace_vercel_plain production NEXT_PUBLIC_SENTRY_DSN 'xxx'
66 |
67 | replace_vercel_plain preview SENTRY_AUTH_TOKEN 'xxx'
68 | replace_vercel_plain production SENTRY_AUTH_TOKEN 'xxx'
69 |
70 | replace_vercel_plain preview SENTRY_URL 'xxx'
71 | replace_vercel_plain production SENTRY_URL 'xxx'
72 |
73 | replace_vercel_plain preview SENTRY_ORG 'xxx'
74 | replace_vercel_plain production SENTRY_ORG 'xxx'
75 |
76 | replace_vercel_plain preview SENTRY_PROJECT 'xxx'
77 | replace_vercel_plain production SENTRY_PROJECT 'xxx'
78 |
79 |
80 | replace_vercel_plain preview HASURA_ADMIN_SECRET 'xxx'
81 | replace_vercel_plain production HASURA_ADMIN_SECRET 'xxx'
82 |
83 | replace_vercel_plain preview HASURA_PROD_SERVER_URL 'xxx'
84 | replace_vercel_plain production HASURA_PROD_SERVER_URL 'xxx'
85 |
86 | replace_vercel_plain preview HASURA_PROD_ADMIN_SECRET 'xxx'
87 | replace_vercel_plain production HASURA_PROD_ADMIN_SECRET 'xxx'
88 |
89 | replace_vercel_plain preview NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT 'xxx'
90 | replace_vercel_plain production NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT 'xxx'
91 |
92 | replace_vercel_plain preview NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT_SUBSCRIPTION 'xxx'
93 | replace_vercel_plain production NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT_SUBSCRIPTION 'xxx'
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/components/forms/FormImage/FormImage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Story, Meta } from '@storybook/react/types-6-0'
3 | import { FormImage, FormImageProps } from './FormImage'
4 |
5 | export default {
6 | title: 'Component/Forms/FormImage',
7 | component: FormImage,
8 | argTypes: {
9 | register: {
10 | table: {
11 | disable: true,
12 | },
13 | },
14 | validationErrors: {
15 | table: {
16 | disable: true,
17 | },
18 | },
19 | },
20 | } as Meta
21 |
22 | const Template: Story = (args) =>
23 |
24 | export const Image_Default = Template.bind({})
25 | Image_Default.args = {
26 | name: 'image_name',
27 | defaultValue: 'https://media.altphotos.com/cache/images/2020/04/14/05/752/flowers-spring.jpg',
28 | register: () => {
29 | /* noop */
30 | },
31 | watch: () => {
32 | /* noop */
33 | },
34 | validationErrors: {},
35 | }
36 |
37 | export const Image_Height = Template.bind({})
38 | Image_Height.args = {
39 | name: 'image_name',
40 | defaultValue: 'https://media.altphotos.com/cache/images/2020/04/14/05/752/flowers-spring.jpg',
41 | register: () => {
42 | /* noop */
43 | },
44 | watch: () => {
45 | /* noop */
46 | },
47 | validationErrors: {},
48 | height: 400,
49 | }
50 |
51 | export const Image_Width = Template.bind({})
52 | Image_Width.args = {
53 | name: 'image_name',
54 | defaultValue: 'https://media.altphotos.com/cache/images/2020/04/14/05/752/flowers-spring.jpg',
55 | register: () => {
56 | /* noop */
57 | },
58 | watch: () => {
59 | /* noop */
60 | },
61 | validationErrors: {},
62 | width: 400,
63 | }
64 |
65 | export const Image_Small = Template.bind({})
66 | Image_Small.args = {
67 | name: 'image_name',
68 | defaultValue: 'https://media.altphotos.com/cache/images/2020/04/14/05/752/flowers-spring.jpg',
69 | register: () => {
70 | /* noop */
71 | },
72 | watch: () => {
73 | /* noop */
74 | },
75 | validationErrors: {},
76 | width: 100,
77 | height: 100,
78 | }
79 |
80 | export const Image_Empty = Template.bind({})
81 | Image_Empty.args = {
82 | name: 'image_name',
83 | placeholder: 'Select an Image',
84 | register: () => {
85 | /* noop */
86 | },
87 | watch: () => {
88 | /* noop */
89 | },
90 | width: 400,
91 | validationErrors: {
92 | text_with_error: {
93 | message: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
94 | },
95 | },
96 | }
97 |
98 | export const Image_Invalid_Url = Template.bind({})
99 | Image_Invalid_Url.args = {
100 | name: 'image_name',
101 | defaultValue: 'https://invalid-url',
102 | placeholder: 'Select an Image',
103 | register: () => {
104 | /* noop */
105 | },
106 | watch: () => {
107 | /* noop */
108 | },
109 | validationErrors: {
110 | text_with_error: {
111 | message: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
112 | },
113 | },
114 | }
115 |
116 | export const Image_Error = Template.bind({})
117 | Image_Error.args = {
118 | name: 'image_name',
119 | defaultValue: 'https://invalid-url',
120 | placeholder: 'Select an Image',
121 | register: () => {
122 | /* noop */
123 | },
124 | watch: () => {
125 | /* noop */
126 | },
127 | validationErrors: {
128 | text_with_error: {
129 | message: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
130 | },
131 | },
132 | }
133 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contact@elitizon.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/components/forms/FormTextarea/FormTextarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Story, Meta } from '@storybook/react/types-6-0'
3 | import { FormTextarea } from './FormTextarea'
4 | import { FormBaseProps } from '../FormBaseProps'
5 |
6 | export default {
7 | title: 'Component/Forms/FormTextarea',
8 | component: FormTextarea,
9 | argTypes: {
10 | register: {
11 | table: {
12 | disable: true,
13 | },
14 | },
15 | validationErrors: {
16 | table: {
17 | disable: true,
18 | },
19 | },
20 | },
21 | } as Meta
22 |
23 | const Template: Story = (args) =>
24 |
25 | export const Textarea_Default_Title = Template.bind({})
26 | Textarea_Default_Title.args = {
27 | name: 'text-area-name',
28 | register: () => {
29 | /* noop */
30 | },
31 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
32 | validationErrors: {},
33 | }
34 |
35 | export const Textarea_Label = Template.bind({})
36 | Textarea_Label.args = {
37 | label: 'Label:',
38 | name: 'text-area-name',
39 | register: () => {
40 | /* noop */
41 | },
42 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
43 | validationErrors: {},
44 | }
45 |
46 | export const Textarea_Label_Description = Template.bind({})
47 | Textarea_Label_Description.args = {
48 | label: 'Label:',
49 | labelDescription: 'this is a description',
50 | name: 'text-area-name',
51 | register: () => {
52 | /* noop */
53 | },
54 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
55 | validationErrors: {},
56 | }
57 |
58 | export const Textarea_Error = Template.bind({})
59 | Textarea_Error.args = {
60 | name: 'text_area_name',
61 | register: () => {
62 | /* noop */
63 | },
64 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
65 | validationErrors: {
66 | text_area_name: {
67 | message: 'This is validation error!',
68 | },
69 | },
70 | }
71 |
72 | export const Textarea_CustomClassName_Large = Template.bind({})
73 | Textarea_CustomClassName_Large.args = {
74 | name: 'text_area_name',
75 | register: () => {
76 | /* noop */
77 | },
78 | className: 'text-lg h-64',
79 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
80 | validationErrors: {},
81 | }
82 |
83 | export const Textarea_CustomClassName_Normal = Template.bind({})
84 | Textarea_CustomClassName_Normal.args = {
85 | name: 'text_area_name',
86 | register: () => {
87 | /* noop */
88 | },
89 | className: 'text-base',
90 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
91 | validationErrors: {},
92 | }
93 |
94 | export const Textarea_CustomClassName_Small = Template.bind({})
95 | Textarea_CustomClassName_Small.args = {
96 | name: 'text_area_name',
97 | register: () => {
98 | /* noop */
99 | },
100 | className: 'text-sm h-20',
101 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
102 | validationErrors: {},
103 | }
104 |
105 | export const Textarea_CustomClassName_Tiny = Template.bind({})
106 | Textarea_CustomClassName_Tiny.args = {
107 | name: 'text_area_name',
108 | register: () => {
109 | /* noop */
110 | },
111 | className: 'h-24 input-xs',
112 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
113 | validationErrors: {},
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/forms/FormLocalImage/FormLocalImage.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, useEffect, useState } from 'react'
2 | import classnames from 'classnames'
3 | import { FormBaseProps } from '../FormBaseProps'
4 | import { FormLabel } from '../FormLabel'
5 | import { FiCamera } from 'react-icons/fi'
6 |
7 | export interface FormLocalImageProps extends FormBaseProps {
8 | width?: string | number
9 | height?: string | number
10 | }
11 |
12 | export const FormLocalImage: React.FC = ({
13 | label,
14 | labelDescription,
15 | name,
16 | width = '100%',
17 | height = 200,
18 | placeholder,
19 | register,
20 | defaultValue = null,
21 | validationErrors,
22 | className = '',
23 | }) => {
24 | const [image, imageSet] = useState<{ url: string | null; name: string | null }>({
25 | url: null,
26 | name: null,
27 | })
28 | useEffect(() => {
29 | if (defaultValue) {
30 | imageSet({ url: defaultValue, name: null })
31 | }
32 | }, [defaultValue])
33 |
34 | const handleImage = (e: ChangeEvent) => {
35 | if (e.target?.files?.length !== 0) {
36 | const imageObj = {
37 | url: URL.createObjectURL(e.target.files?.[0]) || null,
38 | name: e.target.files?.[0]?.name || null,
39 | }
40 | imageSet(imageObj)
41 | }
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
{image.name}
50 |
56 |
57 | {image?.url ? (
58 |
64 | ) : (
65 |
66 | {' '}
67 |
68 | )}
69 |
70 |
71 |
81 |
82 | {placeholder}
83 |
92 |
93 |
94 |
95 | {validationErrors?.[name] && (
96 |
97 | {validationErrors?.[name]?.message}
98 |
99 | )}
100 |
101 |
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/src/docs/prism.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Prism with Next.js'
3 | description: 'Example using Prism / Markdown with Next.js including switching syntax highlighting themes.'
4 | image_url: 'https://brazil.progress.im/sites/default/files/styles/content_full/public/istock-869670360.jpg?itok=QsVJU6NC'
5 | date: '2021-07-06'
6 | ---
7 |
8 | # Using Prism with Next.js
9 |
10 | [**Prism**](https://prismjs.com/) is a popular syntax highlighter commonly used with Markdown.
11 | This example shows how to use Prism with [**Next.js**](https://nextjs.org/). Use the theme dropdown
12 | in the header to switch syntax highlighting themes.
13 |
14 | Next.js uses `getStaticPaths`/`getStaticProps` to generate [static pages](https://nextjs.org/docs/basic-features/data-fetching). These functions are _not_ bundled client-side, so you can **write server-side code directly**. For example, you can read Markdown files from the filesystem (`fs`) – including parsing front matter with [gray-matter](https://github.com/jonschlinkert/gray-matter). For example, let's assume you have a Markdown file located at `docs/prism.md`.
15 |
16 | We can retrieve that file's contents using `getDocBySlug('prism')`.
17 |
18 | ```ts
19 | // utils/docs.ts
20 |
21 | import fs from 'fs'
22 | import { join } from 'path'
23 | import matter from 'gray-matter'
24 |
25 | const docsDirectory = join(process.cwd(), 'src', 'docs')
26 |
27 | export function getDocBySlug(slug: string) {
28 | const realSlug = slug.replace(/\.md$/, '')
29 | const fullPath = join(docsDirectory, `${realSlug}.md`)
30 | const fileContents = fs.readFileSync(fullPath, 'utf8')
31 |
32 | const { data, content } = matter(fileContents)
33 |
34 | return { slug: realSlug, meta: data, content }
35 | }
36 |
37 | export function getAllDocs() {
38 | const slugs = fs.readdirSync(docsDirectory)
39 | const docs = slugs.map((slug) => getDocBySlug(slug))
40 |
41 | return docs
42 | }
43 | ```
44 |
45 | Then, we can **transform** the raw Markdown into HTML using [remark](https://github.com/remarkjs/remark) plugins.
46 |
47 | ```ts
48 | // lib/markdown.ts
49 |
50 | import remark from 'remark'
51 | import html from 'remark-html'
52 | import prism from 'remark-prism'
53 | import { VFileCompatible } from 'vfile'
54 |
55 | export const markdownToHtml = async (markdown: VFileCompatible) => {
56 | const result = await remark().use(html).use(prism).process(markdown)
57 | return result.toString()
58 | }
59 | ```
60 |
61 | Passing the `content` returned by `getDocBySlug('prism')` into `markdownToHtml(content)`
62 | would convert a Markdown file like this:
63 |
64 | ````markdown
65 | ---
66 | title: 'My First doc'
67 | description: 'My very first doc'
68 | ---
69 |
70 | # My First doc
71 |
72 | I **love** using [Next.js](https://nextjs.org/)
73 |
74 | ```js
75 | const doc = getDocBySlug(params.slug)
76 | ```
77 | ````
78 |
79 | into this HTML, which includes the proper elements and class names.
80 |
81 | ```html
82 | My First doc
83 | I love using Next.js
84 |
91 | ```
92 |
93 | ## Deploy Your Own
94 |
95 | View the [**source code**](https://github.com/nextjs-opinionated/nextjs-opinionated-hasura) and deploy your own. You can add new Markdown files to `src/docs/` and see them live instantly!
96 |
97 | [](https://vercel.com/import/git?c=1&s=https://github.com/nextjs-opinionated/nextjs-opinionated-hasura)
98 |
--------------------------------------------------------------------------------
/src/components/forms/FormSelect/FormSelect.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Story, Meta } from '@storybook/react/types-6-0'
3 | import { EMPTY_SELECT_OPTION_VALUE, FormSelect, SelectProps } from './FormSelect'
4 |
5 | export default {
6 | title: 'Component/Forms/FormSelect',
7 | component: FormSelect,
8 | argTypes: {
9 | register: {
10 | table: {
11 | disable: true,
12 | },
13 | },
14 | validationErrors: {
15 | table: {
16 | disable: true,
17 | },
18 | },
19 | },
20 | } as Meta
21 |
22 | const Template: Story = (args) =>
23 |
24 | const FIVE_OPTIONS = [
25 | { value: 'it0', label: 'item 0' },
26 | { value: 'it1', label: 'item 1' },
27 | { value: 'it2', label: 'item 2' },
28 | { value: 'it3', label: 'item 3' },
29 | { value: 'it4', label: 'item 4' },
30 | ]
31 |
32 | export const Select_Without_Label = Template.bind({})
33 | Select_Without_Label.args = {
34 | name: 'select_item',
35 | placeholder: 'Select an Item',
36 | register: () => {
37 | /* noop */
38 | },
39 | validationErrors: {},
40 | options: FIVE_OPTIONS,
41 | }
42 |
43 | export const Select_With_Label = Template.bind({})
44 | Select_With_Label.args = {
45 | label: 'Label',
46 | placeholder: 'Select an Item',
47 | name: 'select_name',
48 | register: () => {
49 | /* noop */
50 | },
51 | validationErrors: {},
52 | options: FIVE_OPTIONS,
53 | }
54 |
55 | export const Select_Empty = Template.bind({})
56 | Select_Empty.args = {
57 | name: 'select_item',
58 | register: () => {
59 | /* noop */
60 | },
61 | validationErrors: {},
62 | options: [],
63 | }
64 |
65 | export const Select_Empty_With_EmptyMessage = Template.bind({})
66 | Select_Empty_With_EmptyMessage.args = {
67 | name: 'select_item',
68 | placeholder: 'Select an Item',
69 | register: () => {
70 | /* noop */
71 | },
72 | validationErrors: {},
73 | options: [],
74 | emptyMessage: 'no items',
75 | }
76 |
77 | export const Select_DefaultValue_ItemEmpty = Template.bind({})
78 | Select_DefaultValue_ItemEmpty.args = {
79 | label: 'Title',
80 | name: 'select_defaultValue',
81 | placeholder: 'Select an Item',
82 | defaultValue: EMPTY_SELECT_OPTION_VALUE,
83 | register: () => {
84 | /* noop */
85 | },
86 |
87 | validationErrors: {},
88 | options: FIVE_OPTIONS,
89 | }
90 |
91 | export const Select_DefaultValue_Item0 = Template.bind({})
92 | Select_DefaultValue_Item0.args = {
93 | label: 'Title',
94 | name: 'select_defaultValue',
95 | placeholder: 'Select an Item',
96 | defaultValue: FIVE_OPTIONS[0].value,
97 | register: () => {
98 | /* noop */
99 | },
100 |
101 | validationErrors: {},
102 | options: FIVE_OPTIONS,
103 | }
104 |
105 | export const Select_DefaultValue_Item1 = Template.bind({})
106 | Select_DefaultValue_Item1.args = {
107 | label: 'Title',
108 | name: 'select_defaultValue',
109 | placeholder: 'Select an Item',
110 | defaultValue: FIVE_OPTIONS[1].value,
111 | register: () => {
112 | /* noop */
113 | },
114 |
115 | validationErrors: {},
116 | options: FIVE_OPTIONS,
117 | }
118 |
119 | export const Select_Error = Template.bind({})
120 | Select_Error.args = {
121 | name: 'text_with_error',
122 | register: () => {
123 | /* noop */
124 | },
125 | placeholder: 'Select an Item',
126 | validationErrors: {
127 | text_with_error: {
128 | message: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
129 | },
130 | },
131 | options: FIVE_OPTIONS,
132 | }
133 |
134 | export const Select_CustomClassName = Template.bind({})
135 | Select_CustomClassName.args = {
136 | name: 'select_CustomClassName',
137 | placeholder: 'Select an Item',
138 | register: () => {
139 | /* noop */
140 | },
141 | validationErrors: {},
142 | className: 'text-lg select-accent',
143 | options: FIVE_OPTIONS,
144 | }
145 |
--------------------------------------------------------------------------------
/src/components/forms/FormInput/FormInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Story, Meta } from '@storybook/react/types-6-0'
3 | import { FormInput, FormInputProps } from './FormInput'
4 |
5 | export default {
6 | title: 'Component/Forms/FormInput',
7 | component: FormInput,
8 | argTypes: {
9 | register: {
10 | table: {
11 | disable: true,
12 | },
13 | },
14 | validationErrors: {
15 | table: {
16 | disable: true,
17 | },
18 | },
19 | },
20 | } as Meta
21 |
22 | const Template: Story = (args) =>
23 |
24 | export const Text_Input_OK = Template.bind({})
25 | Text_Input_OK.args = {
26 | name: 'input_name',
27 | register: () => {
28 | /* noop */
29 | },
30 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at.',
31 | validationErrors: {},
32 | }
33 |
34 | export const Text_Input_Placeholder_Placeholder = Template.bind({})
35 | Text_Input_Placeholder_Placeholder.args = {
36 | title: 'Title',
37 | name: 'input_name',
38 | register: () => {
39 | /* noop */
40 | },
41 |
42 | validationErrors: {},
43 | }
44 |
45 | export const Text_Input_Label = Template.bind({})
46 | Text_Input_Label.args = {
47 | label: 'Label:',
48 | name: 'input_name',
49 | register: () => {
50 | /* noop */
51 | },
52 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at.',
53 | validationErrors: {},
54 | }
55 |
56 | export const Text_Input_Error = Template.bind({})
57 | Text_Input_Error.args = {
58 | name: 'text_with_error',
59 | register: () => {
60 | /* noop */
61 | },
62 | defaultValue: 'this is a title',
63 | validationErrors: {
64 | text_with_error: {
65 | message: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at',
66 | },
67 | },
68 | }
69 |
70 | export const Date_Input = Template.bind({})
71 | Date_Input.args = {
72 | name: 'date_input',
73 | type: 'date',
74 | register: () => {
75 | /* noop */
76 | },
77 | defaultValue: '2021-01-01',
78 | validationErrors: null,
79 | }
80 |
81 | export const Time_Input = Template.bind({})
82 | Time_Input.args = {
83 | name: 'time_input',
84 | type: 'time',
85 | register: () => {
86 | /* noop */
87 | },
88 | defaultValue: '12:30',
89 | validationErrors: null,
90 | }
91 |
92 | export const Text_Input_CustomClassName_Large = Template.bind({})
93 | Text_Input_CustomClassName_Large.args = {
94 | label: 'Label:',
95 | name: 'input_name',
96 | register: () => {
97 | /* noop */
98 | },
99 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at.',
100 | validationErrors: {},
101 | className: 'input-lg',
102 | }
103 |
104 | export const Text_Input_CustomClassName_Normal = Template.bind({})
105 | Text_Input_CustomClassName_Normal.args = {
106 | label: 'Label:',
107 | name: 'input_name',
108 | register: () => {
109 | /* noop */
110 | },
111 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at.',
112 | validationErrors: {},
113 | className: 'input',
114 | }
115 |
116 | export const Text_Input_CustomClassName_Small = Template.bind({})
117 | Text_Input_CustomClassName_Small.args = {
118 | label: 'Label:',
119 | name: 'input_name',
120 | register: () => {
121 | /* noop */
122 | },
123 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at.',
124 | validationErrors: {},
125 | className: 'input-sm',
126 | }
127 |
128 | export const Text_Input_CustomClassName_Tiny = Template.bind({})
129 | Text_Input_CustomClassName_Tiny.args = {
130 | label: 'Label:',
131 | name: 'input_name',
132 | register: () => {
133 | /* noop */
134 | },
135 | defaultValue: 'Lorem ipsum dolor sit amet, consectetur adipiscing. Vestibulum rutrum metus at.',
136 | validationErrors: {},
137 | className: 'input-xs',
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/forms/FormInputColor/FormInputColor.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames'
2 | import React, { useState } from 'react'
3 | import { HexColorPicker } from 'react-colorful'
4 | import { FaCaretDown, FaCaretUp } from 'react-icons/fa'
5 | import { FormBaseProps } from '../FormBaseProps'
6 | import { FormLabel } from '../FormLabel'
7 |
8 | export interface FormInputColorProps extends FormBaseProps {
9 | openOnFocus?: boolean
10 | showHexColorPicker?: boolean
11 | alwaysShowHexColorPicker?: boolean
12 | setValue: any
13 | watch: any
14 | }
15 |
16 | export const FormInputColor: React.FC = ({
17 | label,
18 | labelDescription,
19 | name,
20 | placeholder,
21 | register,
22 | watch,
23 | setValue,
24 | defaultValue,
25 | validationErrors,
26 | className = '',
27 | disabled,
28 | openOnFocus = true,
29 | showHexColorPicker = true,
30 | alwaysShowHexColorPicker = false,
31 | }) => {
32 | const [opened, openedSet] = useState(false)
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
95 |
96 | {/* HexColorPicker */}
97 | {showHexColorPicker && (
98 |
103 | setValue(name, color)} />
104 |
105 | )}
106 |
107 |
108 | {validationErrors?.[name] && (
109 |
110 | {validationErrors?.[name]?.message}
111 |
112 | )}
113 |
114 |
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## [nextjs-opinionated-hasura](https://github.com/saitodisse/nextjs-opinionated-hasura)
2 |
3 | ### - [demo](https://nextjs-opinionated-hasura.vercel.app/)
4 |
5 | ### - [storybook](https://main--60d3401329ef650039bca516.chromatic.com)
6 |
7 | _extends [nextjs-opinionated](https://github.com/saitodisse/nextjs-opinionated) ([demo](https://nextjs-opinionated.vercel.app/)) - base template, without Hasura and Auth0_
8 |
9 | ---
10 |
11 | ### This template includes
12 |
13 | - everything from [nextjs-opinionated](https://github.com/saitodisse/nextjs-opinionated)
14 | - [hasura](https://hasura.io/)
15 | - [graphql-request](https://github.com/prisma-labs/graphql-request)
16 | - [graphql-codegen](https://www.graphql-code-generator.com/)
17 | - [docker-compose](https://docs.docker.com/compose/)
18 | - [react-hook-form](https://react-hook-form.com/)
19 | - [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms)
20 | - [zod](https://github.com/colinhacks/zod)
21 |
22 | ---
23 |
24 | ## Pre-Requirements
25 |
26 | - Install **node** (min 14): https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04
27 |
28 | - Install and configure **docker**. Make sure you can run `docker` command without sudo (Step 2): https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04
29 |
30 | - Install and configure **docker-compose**: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-20-04
31 |
32 | - Install **hasura** cli: https://hasura.io/docs/1.0/graphql/core/hasura-cli/install-hasura-cli.html#install-hasura-cli
33 |
34 | ### Before yarn dev
35 |
36 | ```sh
37 | yarn
38 |
39 | # create .env.local
40 | cp .env.local.example .env.local
41 |
42 | # start your postgress if you have docker
43 | yarn hasuraLocal
44 |
45 | # apply DB in other terminal
46 | yarn migrationLocalApply
47 | ```
48 |
49 | Go to http://localhost:9696, use `admin_secret_local_zzz` as admin secret. You must see messages, tags and message_tag tables
50 |
51 | ---
52 |
53 | ### Run on local database (with docker)
54 |
55 | ```sh
56 | yarn dev
57 | ```
58 |
59 | ---
60 |
61 | ### Run on remote database - heroku (without docker)
62 |
63 | - first create a server using [heroku button](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
64 |
65 | ```sh
66 | yarn devNext
67 | ```
68 |
69 | ## start server
70 |
71 | ```sh
72 | # start docker-compose, generator and next.js
73 | yarn dev
74 | ```
75 |
76 | ---
77 |
78 | ## FREE Deploy on Heroku and Vercel
79 |
80 | - Install Heroku CLI: https://devcenter.heroku.com/articles/heroku-cli#download-and-install
81 | - Install Vercel CLI: https://vercel.com/cli
82 | - Push your repo to github and deploy to your vercel account
83 | - run `vercel link` on your project folder
84 |
85 | - Deploy to heroku with the heroku button (https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
86 | - run `heroku login`
87 |
88 | - `cp .env.production.sh.example .env.production.sh`
89 | - update `HASURA_ADMIN_SECRET`, `NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT`, `HASURA_GRAPHQL_ADMIN_SECRET` at `.env.production.sh` - search/replace for `xxx`
90 |
91 | ---
92 |
93 | ## how to use
94 |
95 | Just fork and run:
96 |
97 | ```sh
98 | # npm install
99 | yarn install
100 |
101 | # next.js site
102 | yarn dev
103 |
104 | # storybook site
105 | yarn storybook
106 |
107 | # tests
108 | yarn test --watch
109 | ```
110 |
111 | ---
112 |
113 | ## configure Auth0
114 |
115 | - https://github.com/auth0/nextjs-auth0
116 |
117 | ---
118 |
119 | ## always updated
120 |
121 | _I'm keeping in sync with nextjs-opinionated base template_
122 |
123 | ```sh
124 | # merge
125 | git pull git@github.com:semantix-engagement-hub/nextjs-opinionated.git main
126 | ```
127 |
128 | _I'm always updating all packages here_
129 |
130 | ```sh
131 | # yarn global add npm-check-updates
132 | ncu -u
133 | yarn
134 | ```
135 |
136 | _tests everything's still works:_
137 |
138 | ```sh
139 | yarn checkAll
140 | ```
141 |
142 | If everything is working, please make a pull request
143 |
144 | ---
145 |
146 | _based on [https://github.com/elitizon/nextjs-tailwind-storybook](https://github.com/elitizon/nextjs-tailwind-storybook)_
147 |
--------------------------------------------------------------------------------
/src/pages/typed-fetch-examples/typedFetch-react-query.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import toString from 'lodash/toString'
3 | import Head from 'next/head'
4 | import React, { useState, useEffect } from 'react'
5 | import { CodeBlock } from '../../components/CodeBlock/CodeBlock'
6 | import { Layout } from '../../components/Layout/Layout'
7 | import { LinksList } from '../../model/site/LinksList'
8 | import typedFetch from '../../utils/typedFetch/typedFetch'
9 | import {
10 | Fetch_tester_api_get,
11 | fetch_tester_api_get_Config,
12 | } from '../../model/api-models/typed-fetch-examples/Fetch_tester_api_get'
13 | // import { Fetch_tester_api_post, fetch_tester_api_post_Config } from '../api/fetch_tester_api_post'
14 | import { useQuery } from 'react-query'
15 | import classnames from 'classnames'
16 | import {
17 | Fetch_tester_api_post,
18 | fetch_tester_api_post_Config,
19 | } from '../../model/api-models/typed-fetch-examples/Fetch_tester_api_post'
20 |
21 | export default function Page() {
22 | const [fetchResultJSON, fetchResultJSONSet] = useState({})
23 | const [forceError, forceErrorSet] = useState(false)
24 |
25 | const getResultObj = useQuery('fetch_tester_api_get_Key', async () => {
26 | const resultObj = await typedFetch<
27 | Fetch_tester_api_get['input'],
28 | Fetch_tester_api_get['output']
29 | >({
30 | ...fetch_tester_api_get_Config,
31 | inputData: {
32 | some_string: 'Ueba!',
33 | divide_by: toString(5),
34 | force_error: toString(forceError),
35 | },
36 | })
37 |
38 | fetchResultJSONSet((d) => ({ ...d, get: resultObj }))
39 | return resultObj
40 | })
41 |
42 | const postResultObj = useQuery('fetch_tester_api_post_Key', async () => {
43 | const resultObj = await typedFetch<
44 | Fetch_tester_api_post['input'],
45 | Fetch_tester_api_post['output']
46 | >({
47 | ...fetch_tester_api_post_Config,
48 | inputData: {
49 | some_string: 'Ueba!',
50 | divide_by: 5,
51 | force_error: forceError,
52 | },
53 | })
54 |
55 | fetchResultJSONSet((d) => ({ ...d, post: resultObj }))
56 | return resultObj
57 | })
58 |
59 | useEffect(() => {
60 | fetchResultJSONSet({ get: getResultObj.data, post: postResultObj.data })
61 | }, [getResultObj, postResultObj])
62 |
63 | return (
64 | <>
65 |
66 | TypedFetch Examples with React Query
67 |
68 |
69 |
72 | TypedFetch Examples with React Query
73 | {process.env.NEXT_PUBLIC_SITE_NAME}
74 |
75 | }
76 | menuItems={Object.values(LinksList)}
77 | >
78 | {/* text */}
79 |
80 |
TypedFetch Examples with React Query
81 |
82 | {/* buttons */}
83 |
84 | {
88 | await getResultObj.refetch()
89 | }}
90 | >
91 | refetch get
92 |
93 | {
97 | await postResultObj.refetch()
98 | }}
99 | >
100 | refetch post
101 |
102 | {
109 | forceErrorSet((f) => !f)
110 | }}
111 | >
112 | force error
113 |
114 |
115 |
116 |
117 |
118 |
119 | >
120 | )
121 | }
122 |
--------------------------------------------------------------------------------