├── .nvmrc ├── src ├── shared │ ├── hooks │ │ └── .gitkeep │ ├── components │ │ ├── form │ │ │ ├── .gitkeep │ │ │ └── control │ │ │ │ ├── index.d.ts │ │ │ │ └── number │ │ │ │ └── TextField.tsx │ │ └── primitives │ │ │ ├── .gitkeep │ │ │ └── layout │ │ │ └── Layout.tsx │ ├── theme │ │ ├── theme.ts │ │ └── colors.ts │ └── providers │ │ ├── GraphQLProvider.tsx │ │ └── ThemeProvider.tsx ├── @types │ ├── env.d.ts │ ├── vite-env.d.ts │ ├── app.d.ts │ ├── styled-components.d.ts │ └── gen │ │ └── graphql.d.ts ├── service │ ├── SumService.ts │ └── SumService.spec.ts ├── main.tsx ├── routing │ ├── AllRoutes.tsx │ └── routes.tsx ├── __mocks__ │ └── mock-graphql-client.ts ├── i18n │ ├── UserAtom.tsx │ ├── MessageSourceLoader.tsx │ ├── translations.ts │ ├── MessageSourceContext.tsx │ └── useMessageSource.ts ├── screens │ ├── about │ │ └── AboutPage.tsx │ ├── home │ │ ├── HomePage.tsx │ │ └── HomePage.spec.tsx │ └── forms │ │ ├── Styles.tsx │ │ └── Forms.tsx ├── App.tsx └── favicon.svg ├── .husky └── pre-commit ├── babel-plugin-macros.config.js ├── vercel.json ├── .env ├── codegen.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── .graphqlconfig ├── jest.setup.ts ├── jest.config.js ├── babel.config.js ├── vite.config.ts ├── tsconfig.json ├── index.html ├── scripts └── download-graphql-schema.mjs ├── .eslintrc.js ├── package.json └── schema.graphql /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.5.0 -------------------------------------------------------------------------------- /src/shared/hooks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/shared/components/form/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/shared/components/primitives/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run pre-commit 5 | -------------------------------------------------------------------------------- /babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styledComponents: { 3 | pure: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [{ "handle": "filesystem" }, { "src": "/(.*)", "dest": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_HASURA_ENDPOINT= 2 | VITE_HASURA_ADMIN_PASSWORD= -------------------------------------------------------------------------------- /src/@types/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | VITE_HASURA_ENDPOINT: string 3 | VITE_HASURA_ADMIN_PASSWORD: string 4 | } 5 | -------------------------------------------------------------------------------- /src/service/SumService.ts: -------------------------------------------------------------------------------- 1 | const sum = (a: number, b: number): number => a + b 2 | 3 | export const SumService = { 4 | sum, 5 | } 6 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'schema.graphql' 3 | generates: 4 | src/@types/gen/graphql.d.ts: 5 | plugins: 6 | - 'typescript' 7 | -------------------------------------------------------------------------------- /src/@types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImportMetaEnv { 3 | VITE_HASURA_ENDPOINT: string 4 | VITE_HASURA_ADMIN_PASSWORD: string 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | package-lock.json 7 | yarn.lock 8 | 9 | # idea 10 | .idea 11 | *.iml 12 | !.idea/prettier.xml 13 | !.idea/codeStyles/codeStyleConfig.xml -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { App } from 'src/App' 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root'), 10 | ) 11 | -------------------------------------------------------------------------------- /src/routing/AllRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { useRoutes } from 'react-router-dom' 3 | import { ROUTES } from 'src/routing/routes' 4 | 5 | export const AllRoutes = (): ReactElement | null => { 6 | return useRoutes(ROUTES) 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "useTabs": false, 9 | "endOfLine": "lf", 10 | "jsxSingleQuote": false 11 | } -------------------------------------------------------------------------------- /src/@types/app.d.ts: -------------------------------------------------------------------------------- 1 | export type AppLocale = 'en' | 'de' | 'it' | 'fr' 2 | 3 | export type AppUser = { 4 | username: string 5 | email: string 6 | roles: AppRoles[] 7 | anonymous: boolean 8 | locale: AppLocale 9 | } 10 | 11 | export type AppRoles = 'ADMIN' | 'USER' 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "graphql.vscode-graphql", 5 | "esbenp.prettier-vscode", 6 | "kamikillerto.vscode-colorize", 7 | "dbaeumer.vscode-eslint", 8 | "jpoissonnier.vscode-styled-components" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/service/SumService.spec.ts: -------------------------------------------------------------------------------- 1 | import { SumService } from 'src/service/SumService' 2 | 3 | describe('SumService', () => { 4 | it('should sum two numbers', () => { 5 | // given 6 | 7 | // when 8 | const result = SumService.sum(2, 3) 9 | 10 | // then 11 | expect(result).toBe(5) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/shared/components/form/control/index.d.ts: -------------------------------------------------------------------------------- 1 | export type FinalFormInput = 2 | | 'onBlur' 3 | | 'onChange' 4 | | 'onFocus' 5 | | 'type' 6 | | 'value' 7 | | 'checked' 8 | | 'multiple' 9 | | 'defaultValue' 10 | | 'helperText' 11 | 12 | export interface Option { 13 | label: string 14 | value: number | string 15 | } 16 | -------------------------------------------------------------------------------- /src/@types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import { ColorsType } from 'src/shared/theme/colors' 2 | 3 | declare module 'styled-components' { 4 | // eslint-disable-next-line no-restricted-imports 5 | import 'styled-components' 6 | 7 | export interface DefaultTheme { 8 | colors: typeof ColorsType 9 | spacing: (_: number) => string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/__mocks__/mock-graphql-client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'urql' 2 | import { fromValue, never } from 'wonka' 3 | 4 | export const mockGraphQLClient = (response: unknown): Client => 5 | ({ 6 | executeQuery: jest.fn(() => fromValue(response)), 7 | executeMutation: jest.fn(() => never), 8 | executeSubscription: jest.fn(() => never), 9 | } as unknown as Client) 10 | -------------------------------------------------------------------------------- /src/i18n/UserAtom.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | import { AppLocale, AppUser } from 'src/@types/app' 3 | 4 | const defaultUser: AppUser = { 5 | username: '', 6 | email: '', 7 | roles: [], 8 | anonymous: true, 9 | locale: 'en', 10 | } 11 | 12 | export const userAtom = atom(defaultUser) 13 | 14 | export const userLocaleAtom = atom((get) => get(userAtom).locale) 15 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hasura @ Cloud", 3 | "schemaPath": "schema.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Default GraphQL Endpoint": { 7 | "url": "https://direct-stag-12.hasura.app/v1/graphql", 8 | "headers": { 9 | "x-hasura-admin-secret": "${env:VITE_HASURA_ADMIN_PASSWORD}" 10 | }, 11 | "introspect": false 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect' 6 | // jest doesn't run in a browser we don't have fetch 🤡 7 | import 'isomorphic-unfetch' 8 | 9 | process.env.VITE_HASURA_ADMIN_PASSWORD = 'JEST_HASURA_ADMIN_PASSWORD' 10 | process.env.VITE_HASURA_ENDPOINT = 'JEST_HASURA_ENDPOINT' 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], 4 | transform: { 5 | '^.+\\.(ts|tsx)$': '/node_modules/babel-jest', 6 | }, 7 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$'], 8 | moduleDirectories: ['node_modules', '.'], 9 | // setupFilesAfterEnv: ['jest-extended'], 10 | resetMocks: true, 11 | testEnvironment: 'jsdom', 12 | setupFilesAfterEnv: ['/jest.setup.ts'], 13 | } 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = 2 | process.env['NODE_ENV'] !== 'production' 3 | ? [ 4 | ['@babel/preset-env', { targets: { node: 'current' } }], 5 | [ 6 | '@babel/preset-react', 7 | { 8 | runtime: 'automatic', 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ] 13 | : null 14 | 15 | const plugins = [['macros']] 16 | if (process.env['NODE_ENV'] !== 'production') { 17 | plugins.push(['babel-plugin-transform-vite-meta-env']) 18 | } 19 | 20 | module.exports = { presets, plugins } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import graphqlPlugin from '@rollup/plugin-graphql' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | import { defineConfig } from 'vite' 5 | import macrosPlugin from 'vite-plugin-babel-macros' 6 | import notifier from 'vite-plugin-notifier' 7 | 8 | const projectRootDir = path.resolve(__dirname) 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [macrosPlugin(), react(), graphqlPlugin(), notifier()], 12 | resolve: { 13 | alias: [{ find: 'src', replacement: path.resolve(projectRootDir, 'src') }], 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/shared/components/primitives/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { NavLink, Outlet } from 'react-router-dom' 3 | import styled from 'styled-components/macro' 4 | 5 | const LinkStyled = styled(NavLink)` 6 | &[aria-current] { 7 | width: 500px; 8 | color: #9378d9; 9 | } 10 | ` 11 | export const Layout = (): ReactElement => { 12 | return ( 13 |
14 | Home 15 | About 16 | Forms 17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/theme/theme.ts: -------------------------------------------------------------------------------- 1 | import { createColors } from 'src/shared/theme/colors' 2 | import { createGlobalStyle, DefaultTheme } from 'styled-components/macro' 3 | 4 | const spacing = (x: number): string => `${x * 8}px` 5 | 6 | export const createTheme = (mode: 'light' | 'dark'): DefaultTheme => { 7 | return { colors: createColors(mode), spacing } 8 | } 9 | 10 | export const GlobalStyles = createGlobalStyle` 11 | body { 12 | background: ${({ theme }) => theme.colors.global.body}; 13 | color: ${({ theme }) => theme.colors.global.text}; 14 | transition: all .25s; 15 | font-family: 'Inter', sans-serif; 16 | } 17 | ` 18 | -------------------------------------------------------------------------------- /src/routing/routes.tsx: -------------------------------------------------------------------------------- 1 | import { AboutPage } from 'src/screens/about/AboutPage' 2 | import { FormsPage } from 'src/screens/forms/Forms' 3 | import { HomePage } from 'src/screens/home/HomePage' 4 | import { Layout } from 'src/shared/components/primitives/layout/Layout' 5 | 6 | export const ROUTES = [ 7 | { 8 | path: '/', 9 | element: , 10 | 11 | children: [ 12 | { path: '/', element: }, 13 | { 14 | path: 'about', 15 | element: , 16 | }, 17 | { 18 | path: 'forms', 19 | element: , 20 | }, 21 | ], 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /src/screens/about/AboutPage.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { useMessageSource } from 'src/i18n/useMessageSource' 3 | import { userAtom } from 'src/i18n/UserAtom' 4 | import { useAtom } from 'jotai' 5 | 6 | export const AboutPage = (): ReactElement => { 7 | const { getMessage } = useMessageSource() 8 | const [user] = useAtom(userAtom) 9 | return ( 10 |
11 |

About page

12 |
user
13 |
user is anonymous: {`${user.anonymous}`}
14 |
username is: {user.username}
15 |
{getMessage('label.confirm')}
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/i18n/MessageSourceLoader.tsx: -------------------------------------------------------------------------------- 1 | import { memo, ReactNode } from 'react' 2 | import { Provider as MessageSourceProvider } from 'src/i18n/MessageSourceContext' 3 | import { useAtom } from 'jotai' 4 | import { userLocaleAtom } from 'src/i18n/UserAtom' 5 | import { resolveTranslations } from 'src/i18n/translations' 6 | 7 | const MessageSourceLoader = memo(({ children }: { children: ReactNode }) => { 8 | const [locale] = useAtom(userLocaleAtom) 9 | 10 | const translations = resolveTranslations(locale) 11 | 12 | return {children} 13 | }) 14 | export { MessageSourceLoader } 15 | -------------------------------------------------------------------------------- /src/i18n/translations.ts: -------------------------------------------------------------------------------- 1 | import { AppLocale } from 'src/@types/app' 2 | 3 | interface Translations { 4 | [key: string]: { 5 | en: string 6 | de: string 7 | it: string 8 | fr: string 9 | } 10 | } 11 | 12 | const TRANSLATIONS: Translations = { 13 | 'label.confirm': { 14 | en: 'Confirm', 15 | de: 'Bestätigen', 16 | it: 'Confermare', 17 | fr: 'Confirmer', 18 | }, 19 | } 20 | 21 | export const resolveTranslations = (locale: AppLocale): Record => 22 | Object.keys(TRANSLATIONS).reduce((cache, key) => { 23 | cache[key] = TRANSLATIONS[key][locale] 24 | return cache 25 | }, {} as Record) 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": false, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "strictNullChecks": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "src/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["./src"] 24 | } 25 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { BrowserRouter } from 'react-router-dom' 3 | import { AllRoutes } from 'src/routing/AllRoutes' 4 | import { GraphQLClientProvider } from 'src/shared/providers/GraphQLProvider' 5 | import { ThemeProvider } from 'src/shared/providers/ThemeProvider' 6 | import { MessageSourceLoader } from 'src/i18n/MessageSourceLoader' 7 | 8 | export const App = (): ReactElement => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite React TS Starter 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "cSpell.words": ["Todos", "VITE", "codegen", "dotenv", "flowtype", "reexecute", "wonka"], 4 | "typescript.preferences.importModuleSpecifier": "non-relative", 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[typescript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true, 13 | "editor.codeActionsOnSave": { 14 | "source.organizeImports": true 15 | }, 16 | "[markdown]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "colorize.languages": ["javascriptreact", "typescriptreact", "typescript", "javascript", "css", "scss"] 20 | } 21 | -------------------------------------------------------------------------------- /src/i18n/MessageSourceContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export type MessageSourceContextShape = { 4 | [key: string]: string 5 | } 6 | 7 | /** 8 | * Initial Context value, an empty object. 9 | */ 10 | const empty = {} 11 | 12 | /** 13 | * A React Context which holds the translations map. 14 | */ 15 | const MessageSourceContext = createContext(empty) 16 | MessageSourceContext.displayName = 'MessageSourceContext' 17 | 18 | /** 19 | * The MessageSourceContext object. 20 | */ 21 | export { MessageSourceContext } 22 | 23 | /** 24 | * Example usage: 25 | * 26 | * const translations = await fetch('/api/rest/texts?lang=en'); 27 | * 28 | * 29 | * ... 30 | * 31 | */ 32 | export const { Provider, Consumer } = MessageSourceContext 33 | -------------------------------------------------------------------------------- /src/screens/home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from 'react' 2 | import { Query_Root } from 'src/@types/gen/graphql' 3 | import styled from 'styled-components/macro' 4 | import { gql, useQuery } from 'urql' 5 | 6 | const Header = styled.h1`` 7 | 8 | const TodoFrag = gql` 9 | fragment TodoShared on todo { 10 | id 11 | description 12 | done 13 | } 14 | ` 15 | 16 | const fetchTodos = gql` 17 | query MyQuery { 18 | todo { 19 | id 20 | ...TodoShared 21 | } 22 | } 23 | ${TodoFrag} 24 | ` 25 | 26 | export const HomePage = (): ReactElement => { 27 | const [{ data }] = useQuery<{ todo: Query_Root['todo'] }>({ query: fetchTodos }) 28 | return ( 29 |
30 |
Welcome to my home page
31 |

Home Page

32 |
    {data && data.todo.map((todo, i) =>
  • {todo.description}
  • )}
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/screens/home/HomePage.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { HomePage } from 'src/screens/home/HomePage' 3 | import { mockGraphQLClient } from 'src/__mocks__/mock-graphql-client' 4 | import { Client, Provider } from 'urql' 5 | 6 | describe('HomePage', () => { 7 | let mockClient = {} as Client 8 | 9 | // jest will reset mocks after each test - that's why we re-initialize the mock client 10 | // see `jest.config.js` -> `resetMocks` configuration property 11 | beforeEach(() => { 12 | mockClient = mockGraphQLClient({ 13 | data: { 14 | todo: [ 15 | { id: 1, description: 'Post title 1', done: false }, 16 | { id: 3, description: 'Post title 2', content: true }, 17 | ], 18 | }, 19 | }) 20 | }) 21 | it('should render a list of todos', () => { 22 | const { container } = render( 23 | 24 | 25 | , 26 | ) 27 | const firstTodo = screen.getByText('Post title 1') 28 | const secondTodo = screen.getByText('Post title 2') 29 | 30 | expect(mockClient.executeQuery).toBeCalledTimes(1) 31 | expect(container).toContainElement(firstTodo) 32 | expect(container).toContainElement(secondTodo) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /scripts/download-graphql-schema.mjs: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import { writeFileSync } from 'fs' 3 | import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql' 4 | import fetch from 'node-fetch' 5 | import { dirname, format, join } from 'path' 6 | import { fileURLToPath } from 'url' 7 | 8 | // current file is in /scripts 9 | const currentFile = fileURLToPath(import.meta.url) 10 | 11 | // get the absolute path to /scripts 12 | const scriptsFolder = dirname(currentFile) 13 | 14 | // get back one level to get to the project root dir 15 | const projectRootDir = join(scriptsFolder, '..') 16 | 17 | const dotenvPath = format({ dir: projectRootDir, base: '.env.local' }) 18 | dotenv.config({ path: dotenvPath }) 19 | 20 | const SCHEMA_FILE_NAME = 'schema.graphql' 21 | const HASURA_ENDPOINT = process.env.VITE_HASURA_ENDPOINT 22 | const HASURA_ADMIN_PASSWORD = process.env.VITE_HASURA_ADMIN_PASSWORD 23 | 24 | const response = await fetch(HASURA_ENDPOINT, { 25 | method: 'POST', 26 | headers: { 'Content-Type': 'application/json', 'x-hasura-admin-secret': HASURA_ADMIN_PASSWORD }, 27 | body: JSON.stringify({ query: getIntrospectionQuery() }), 28 | }) 29 | const graphqlSchemaObj = buildClientSchema((await response.json()).data) 30 | const sdlString = printSchema(graphqlSchemaObj) 31 | 32 | const finalDestination = format({ dir: projectRootDir, base: SCHEMA_FILE_NAME }) 33 | writeFileSync(finalDestination, sdlString) 34 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/shared/providers/GraphQLProvider.tsx: -------------------------------------------------------------------------------- 1 | import { devtoolsExchange } from '@urql/devtools' 2 | import { ReactElement, ReactNode, useMemo } from 'react' 3 | import { cacheExchange, CombinedError, createClient, dedupExchange, errorExchange, fetchExchange, Provider } from 'urql' 4 | 5 | const VITE_HASURA_ADMIN_PASSWORD = import.meta.env.VITE_HASURA_ADMIN_PASSWORD 6 | const VITE_HASURA_ENDPOINT = import.meta.env.VITE_HASURA_ENDPOINT 7 | 8 | export const GraphQLClientProvider = ({ children }: { children: ReactNode }): ReactElement => { 9 | const URQL_EXCHANGES = useMemo(() => { 10 | const onError = async (e: CombinedError) => { 11 | const isDenied = e?.graphQLErrors.some((x) => x.extensions?.code === 'access-denied') 12 | const hasNetworkError = e.networkError?.message 13 | 14 | if (isDenied && hasNetworkError) { 15 | console.error('Something happened', e) 16 | } 17 | } 18 | 19 | return [devtoolsExchange, dedupExchange, cacheExchange, errorExchange({ onError }), fetchExchange] 20 | }, []) 21 | 22 | const urqlClient = useMemo( 23 | () => 24 | createClient({ 25 | url: VITE_HASURA_ENDPOINT, 26 | exchanges: URQL_EXCHANGES, 27 | requestPolicy: 'network-only', 28 | fetchOptions: () => ({ 29 | mode: 'cors', 30 | credentials: 'include', 31 | headers: { 32 | 'x-hasura-admin-secret': VITE_HASURA_ADMIN_PASSWORD, 33 | }, 34 | }), 35 | }), 36 | [URQL_EXCHANGES], 37 | ) 38 | return {children} 39 | } 40 | -------------------------------------------------------------------------------- /src/shared/theme/colors.ts: -------------------------------------------------------------------------------- 1 | interface SemanticColors { 2 | global: { 3 | body: string 4 | text: string 5 | } 6 | primary: string 7 | secondary: string 8 | accent: string 9 | } 10 | 11 | export type ColorsType = typeof GENERIC_COLORS & SemanticColors 12 | 13 | export type ThemeMode = 'light' | 'dark' 14 | export const GENERIC_COLORS = { 15 | purple: '#9378d9', 16 | green: '#5BD097', 17 | teal: '#2FD2E3', 18 | orange: '#FFAC53', 19 | grey: { 20 | dark: '#121620', 21 | light: '#f1f1f1', 22 | }, 23 | } 24 | 25 | export const createColors = (mode: ThemeMode): ColorsType => { 26 | const semanticColors = mode === 'dark' ? createDarkColors() : createLightColors() 27 | 28 | return { 29 | ...GENERIC_COLORS, 30 | ...semanticColors, 31 | } 32 | } 33 | 34 | const createLightColors = (): SemanticColors => { 35 | return { 36 | global: { 37 | body: GENERIC_COLORS.grey.light, 38 | text: GENERIC_COLORS.grey.dark, 39 | }, 40 | primary: GENERIC_COLORS.teal, 41 | secondary: GENERIC_COLORS.purple, 42 | accent: GENERIC_COLORS.orange, 43 | } 44 | } 45 | 46 | const createDarkColors = (): SemanticColors => { 47 | return { 48 | global: { 49 | body: GENERIC_COLORS.grey.dark, 50 | text: GENERIC_COLORS.grey.light, 51 | }, 52 | primary: GENERIC_COLORS.teal, 53 | secondary: GENERIC_COLORS.purple, 54 | accent: GENERIC_COLORS.orange, 55 | } 56 | } 57 | 58 | export const COLORS = { 59 | ...GENERIC_COLORS, 60 | primary: GENERIC_COLORS.teal, 61 | secondary: GENERIC_COLORS.purple, 62 | } 63 | -------------------------------------------------------------------------------- /src/shared/providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { FiMoon, FiSun } from 'react-icons/fi' 3 | import { createTheme, GlobalStyles } from 'src/shared/theme/theme' 4 | import styled, { ThemeProvider as StyledThemeProvider } from 'styled-components/macro' 5 | 6 | const getInitialTheme = (): 'dark' | 'light' => { 7 | const savedTheme = window.localStorage.getItem('theme') 8 | return savedTheme ? JSON.parse(savedTheme) : 'dark' 9 | } 10 | 11 | export const Toggler = styled.button` 12 | display: flex; 13 | position: absolute; 14 | right: 20px; 15 | top: 10px; 16 | width: 35px; 17 | height: 35px; 18 | align-items: center; 19 | justify-content: center; 20 | color: ${({ theme }) => theme.colors.global.text}; 21 | background-color: transparent; 22 | transition: all 0.25s; 23 | border: none; 24 | outline: none; 25 | cursor: pointer; 26 | ` 27 | 28 | export const ThemeProvider: React.FC = ({ children }) => { 29 | const [theme, setTheme] = useState<'dark' | 'light'>(getInitialTheme) 30 | 31 | useEffect(() => { 32 | window.localStorage.setItem('theme', JSON.stringify(theme)) 33 | }, [theme]) 34 | const toggleTheme = () => { 35 | theme === 'light' ? setTheme('dark') : setTheme('light') 36 | } 37 | const icon = theme === 'light' ? : 38 | 39 | return ( 40 | 41 | 42 | {icon} 43 | {children} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const fs = require('fs') 3 | 4 | const graphQlSchema = fs.readFileSync('schema.graphql', 'utf8') 5 | module.exports = { 6 | env: { 7 | browser: true, 8 | es2021: true, 9 | }, 10 | settings: { 11 | react: { 12 | version: 'detect', 13 | }, 14 | }, 15 | extends: ['eslint:recommended', 'react-app', 'plugin:@typescript-eslint/recommended', 'prettier'], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 12, 22 | sourceType: 'module', 23 | }, 24 | plugins: ['react', '@typescript-eslint', 'graphql', 'prettier'], 25 | rules: { 26 | '@typescript-eslint/no-explicit-any': 'off', 27 | '@typescript-eslint/no-unused-vars': [ 28 | 'warn', 29 | { 30 | args: 'after-used', 31 | argsIgnorePattern: '^_', 32 | ignoreRestSiblings: true, 33 | }, 34 | ], 35 | '@typescript-eslint/no-empty-function': 'off', 36 | '@typescript-eslint/ban-ts-comment': 'off', 37 | '@typescript-eslint/no-non-null-assertion': 'off', 38 | 'no-restricted-imports': [ 39 | 'error', 40 | { 41 | paths: [ 42 | { 43 | name: 'styled-components', 44 | message: 'Please import from styled-components/macro.', 45 | }, 46 | ], 47 | patterns: ['!styled-components/macro'], 48 | }, 49 | ], 50 | 'graphql/template-strings': [ 51 | 'error', 52 | { 53 | env: 'apollo', 54 | schemaString: graphQlSchema, 55 | tagName: 'gql', 56 | }, 57 | ], 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /src/shared/components/form/control/number/TextField.tsx: -------------------------------------------------------------------------------- 1 | import { TextField as MuiTextField, TextFieldProps as MuiTextFieldProps } from '@material-ui/core' 2 | import { ReactElement } from 'react' 3 | import { Field, FieldRenderProps } from 'react-final-form' 4 | import { useMessageSource } from 'src/i18n/useMessageSource' 5 | import { FinalFormInput } from 'src/shared/components/form/control' 6 | import { useRifm } from 'rifm' 7 | 8 | type TextFieldType = 'password' | 'text' | 'email' | 'tel' 9 | 10 | type TextFieldPropsToBeExcluded = FinalFormInput | 'variant' 11 | 12 | type TextFieldProps = Omit & { 13 | validate?: any 14 | name: string 15 | type?: TextFieldType 16 | $withCheckbox?: boolean 17 | } 18 | 19 | const numberFormat = (str: string) => { 20 | const r = parseInt(str.replace(/[^\d]+/gi, ''), 10) 21 | return r ? r.toLocaleString('en') : '' 22 | } 23 | 24 | const Text = (props: FieldRenderProps): ReactElement => { 25 | const { 26 | input: { name, onChange, onFocus, onBlur, value, type = 'text' }, 27 | meta, 28 | $withCheckbox, 29 | ...rest 30 | } = props 31 | 32 | const muiTextFieldProps = rest as MuiTextFieldProps 33 | const { helperText, ...restMuiTextFieldProps } = muiTextFieldProps 34 | 35 | const { touched, error: errorObject } = meta 36 | const { getMessage } = useMessageSource() 37 | const error = errorObject && getMessage(errorObject.errorKey, errorObject.params) 38 | const invalid = Boolean(touched && error) 39 | 40 | const localOnChange = (value: string) => { 41 | onChange(value) 42 | } 43 | 44 | const rifm = useRifm({ 45 | value, 46 | onChange: localOnChange, 47 | format: numberFormat, 48 | }) 49 | 50 | return ( 51 | 64 | ) 65 | } 66 | export const TextField = (props: TextFieldProps): ReactElement => ( 67 | , HTMLInputElement> component={Text} {...props} /> 68 | ) 69 | -------------------------------------------------------------------------------- /src/i18n/useMessageSource.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import { MessageSourceContext, MessageSourceContextShape } from 'src/i18n/MessageSourceContext' 3 | 4 | export interface MessageSourceApi { 5 | /** 6 | * Retrieves a text message. 7 | * 8 | * Example usage: 9 | * let name, lastName; 10 | * ... 11 | * const message = getMessage('message.key', [name, lastName]); 12 | * 13 | * @param key the key of the message. 14 | * @param params an optional parameters (param0, param1 ...). 15 | */ 16 | getMessage: (key: string, params?: string[]) => string 17 | } 18 | 19 | /** 20 | * Checks whether the given String ends with the given suffix. 21 | * 22 | * @param str the string to test. 23 | * @param suffix the suffix to check. 24 | * @return {boolean} 25 | */ 26 | const endsWith = (str: string, suffix: string): boolean => str.indexOf(suffix, str.length - suffix.length) >= 0 27 | 28 | /** 29 | * Normalizes the given keyPrefix to a defined format. 30 | * 31 | * @param keyPrefix the prefix to normalize. 32 | */ 33 | export const normalizeKeyPrefix = (keyPrefix: string): string => 34 | keyPrefix.length > 0 && !endsWith(keyPrefix, '.') ? `${keyPrefix}.` : keyPrefix 35 | 36 | /** 37 | * Retrieves a text message. 38 | * 39 | * @param textMessages a map which holds the translation pairs. 40 | * @param textKey the key of the message. 41 | * @param params optional placeholder parameters. 42 | * @returns {string} the message or the key itself. 43 | */ 44 | export function getMessageWithParams( 45 | textMessages: MessageSourceContextShape, 46 | textKey: string, 47 | params: string[] = [], 48 | ): string { 49 | const message = textMessages[textKey] || textKey 50 | return params.reduce((msg, current, index) => msg.replace(new RegExp(`\\{${index}\\}`, 'g'), current), message) 51 | } 52 | 53 | /** 54 | * A Hook which which provides the MessageSourceApi. 55 | * 56 | * @param keyPrefix an optional prefix which will be prepended to the lookup key. 57 | */ 58 | export function useMessageSource(keyPrefix?: string): MessageSourceApi { 59 | const textKeys = useContext(MessageSourceContext) 60 | return useMemo(() => { 61 | const keyPrefixToUse = normalizeKeyPrefix(keyPrefix || '') 62 | return { 63 | getMessage(key: string, params?: string[]) { 64 | const textKey = keyPrefixToUse + key 65 | const message = getMessageWithParams(textKeys, textKey, params) 66 | if (message === textKey && textKey !== '' && typeof textKey !== 'undefined') { 67 | if (import.meta.env.DEV) { 68 | console.warn(`You are missing the following key in your "translations.ts" file: [${textKey}]`) 69 | } 70 | } 71 | 72 | return message 73 | }, 74 | } 75 | }, [textKeys, keyPrefix]) 76 | } 77 | -------------------------------------------------------------------------------- /src/screens/forms/Styles.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components/macro' 2 | 3 | const btn = (light: string, dark: string) => css` 4 | white-space: nowrap; 5 | display: inline-block; 6 | border-radius: 5px; 7 | padding: 5px 15px; 8 | font-size: 16px; 9 | color: white; 10 | &:visited { 11 | color: white; 12 | } 13 | background-image: linear-gradient(${light}, ${dark}); 14 | border: 1px solid ${dark}; 15 | &:hover { 16 | background-image: linear-gradient(${light}, ${dark}); 17 | &[disabled] { 18 | background-image: linear-gradient(${light}, ${dark}); 19 | } 20 | } 21 | &:visited { 22 | color: black; 23 | } 24 | &[disabled] { 25 | opacity: 0.6; 26 | cursor: not-allowed; 27 | } 28 | ` 29 | 30 | const btnDefault = css` 31 | ${btn('#ffffff', '#d5d5d5')} color: #555; 32 | ` 33 | 34 | const btnPrimary = btn('#4f93ce', '#285f8f') 35 | 36 | export default styled.div` 37 | font-family: sans-serif; 38 | 39 | h1 { 40 | text-align: center; 41 | color: #222; 42 | } 43 | 44 | & > div { 45 | text-align: center; 46 | } 47 | 48 | a { 49 | display: block; 50 | text-align: center; 51 | color: #222; 52 | } 53 | 54 | form { 55 | max-width: 500px; 56 | margin: 10px auto; 57 | border: 1px solid #ccc; 58 | padding: 20px; 59 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3); 60 | border-radius: 3px; 61 | 62 | & > div { 63 | display: flex; 64 | flex-flow: row nowrap; 65 | line-height: 2em; 66 | margin: 5px; 67 | 68 | & > label { 69 | color: #f4b037; 70 | width: 110px; 71 | font-size: 1em; 72 | line-height: 32px; 73 | } 74 | 75 | & > input, 76 | & > select, 77 | & > textarea { 78 | flex: 1; 79 | padding: 3px 5px; 80 | font-size: 1em; 81 | margin-left: 15px; 82 | border: 1px solid #ccc; 83 | border-radius: 3px; 84 | } 85 | 86 | & > input[type='checkbox'] { 87 | margin-top: 7px; 88 | } 89 | 90 | & > div { 91 | margin-left: 16px; 92 | 93 | & > label { 94 | display: block; 95 | 96 | & > input { 97 | margin-right: 3px; 98 | } 99 | } 100 | } 101 | } 102 | 103 | & > .buttons { 104 | display: flex; 105 | flex-flow: row nowrap; 106 | justify-content: center; 107 | margin-top: 15px; 108 | } 109 | 110 | button { 111 | margin: 0 10px; 112 | 113 | &[type='submit'] { 114 | ${btnPrimary}; 115 | } 116 | 117 | &[type='button'] { 118 | ${btnDefault}; 119 | } 120 | } 121 | 122 | pre { 123 | border: 1px solid #ccc; 124 | background: rgba(0, 0, 0, 0.1); 125 | box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.2); 126 | padding: 20px; 127 | } 128 | } 129 | ` 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "license": "MIT", 4 | "scripts": { 5 | "dev": "vite", 6 | "prepare": "husky install", 7 | "prebuild": "test \"$CI\" = true && npx pnpm install -r --store=node_modules/.pnpm-store || echo skipping pnpm install", 8 | "build": "tsc && vite build", 9 | "serve": "vite preview", 10 | "lint": "eslint \"src/**/*.{tsx,ts}\" && tsc", 11 | "prettier:fix": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json}\"", 12 | "lint:fix": "eslint --ext .jsx,.js,.ts,.tsx --fix src/", 13 | "test": "jest", 14 | "pre-commit": "lint-staged", 15 | "graphql:download:schema": "node scripts/download-graphql-schema.mjs", 16 | "graphql:generate:typescript": "graphql-codegen --config codegen.yml", 17 | "graphql:all": "npm run graphql:download:schema && npm run graphql:generate:typescript" 18 | }, 19 | "lint-staged": { 20 | "*.{jsx,tsx,ts,js,md,json}": [ 21 | "prettier --write" 22 | ], 23 | "*.{jsx,tsx,ts,js}": [ 24 | "eslint --fix" 25 | ] 26 | }, 27 | "dependencies": { 28 | "@emotion/react": "^11.4.1", 29 | "@emotion/styled": "^11.3.0", 30 | "@material-ui/core": "^5.0.0-beta.4", 31 | "@material-ui/styled-engine-sc": "^5.0.0-beta.5", 32 | "final-form": "^4.20.4", 33 | "graphql": "^15.6.1", 34 | "graphql-combine-query": "^1.2.3", 35 | "history": "^5.0.1", 36 | "jotai": "^1.3.8", 37 | "lodash-es": "^4.17.21", 38 | "react": "^17.0.2", 39 | "react-dom": "^17.0.2", 40 | "react-final-form": "^6.5.7", 41 | "react-final-form-listeners": "^1.0.3", 42 | "react-icons": "^4.3.1", 43 | "react-router-dom": "^6.0.0-beta.6", 44 | "rifm": "^0.12.0", 45 | "styled-components": "^5.3.1", 46 | "urql": "^2.0.5" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.15.8", 50 | "@babel/preset-env": "^7.15.8", 51 | "@babel/preset-react": "^7.14.5", 52 | "@babel/preset-typescript": "^7.15.0", 53 | "@graphql-codegen/cli": "^1.21.8", 54 | "@graphql-codegen/introspection": "^1.18.2", 55 | "@graphql-codegen/typescript": "^1.23.0", 56 | "@rollup/plugin-graphql": "^1.0.0", 57 | "@testing-library/jest-dom": "^5.14.1", 58 | "@testing-library/react": "^12.1.2", 59 | "@types/jest": "^26.0.24", 60 | "@types/lodash": "^4.14.175", 61 | "@types/react": "^17.0.27", 62 | "@types/react-dom": "^17.0.9", 63 | "@types/react-router": "^5.1.17", 64 | "@types/styled-components": "^5.1.15", 65 | "@types/testing-library__jest-dom": "^5.14.1", 66 | "@typescript-eslint/eslint-plugin": "^4.33.0", 67 | "@typescript-eslint/parser": "^4.33.0", 68 | "@urql/devtools": "^2.0.3", 69 | "@vitejs/plugin-react": "^1.0.2", 70 | "@vitejs/plugin-react-refresh": "^1.3.6", 71 | "babel-jest": "^27.2.5", 72 | "babel-plugin-macros": "^3.1.0", 73 | "babel-plugin-transform-vite-meta-env": "^1.0.3", 74 | "dotenv": "^10.0.0", 75 | "eslint": "^7.32.0", 76 | "eslint-config-prettier": "^8.3.0", 77 | "eslint-config-react-app": "^6.0.0", 78 | "eslint-plugin-flowtype": "^5.10.0", 79 | "eslint-plugin-graphql": "^4.0.0", 80 | "eslint-plugin-import": "^2.24.2", 81 | "eslint-plugin-jsx-a11y": "^6.4.1", 82 | "eslint-plugin-prettier": "^3.4.1", 83 | "eslint-plugin-react": "^7.26.1", 84 | "eslint-plugin-react-hooks": "^4.2.0", 85 | "husky": "^7.0.2", 86 | "isomorphic-unfetch": "^3.1.0", 87 | "jest": "^27.2.5", 88 | "jest-extended": "^0.11.5", 89 | "lint-staged": "^11.2.2", 90 | "node-fetch": "^2.6.5", 91 | "prettier": "^2.4.1", 92 | "prettier-plugin-organize-imports": "^2.3.4", 93 | "rollup": "^2.58.0", 94 | "typescript": "^4.4.3", 95 | "vite": "^2.6.5", 96 | "vite-plugin-babel-macros": "^1.0.5", 97 | "vite-plugin-notifier": "^0.1.5", 98 | "vite-plugin-svgr": "^0.3.0", 99 | "vite-react-jsx": "^1.1.2", 100 | "wonka": "^4.0.15" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/screens/forms/Forms.tsx: -------------------------------------------------------------------------------- 1 | import Styles from './Styles' 2 | import { Field, Form } from 'react-final-form' 3 | import { ReactElement } from 'react' 4 | import { TextField } from 'src/shared/components/form/control/number/TextField' 5 | 6 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 7 | 8 | const onSubmit = async (values: any) => { 9 | await sleep(300) 10 | window.alert(JSON.stringify(values, null, 2)) 11 | } 12 | 13 | export const FormsPage = (): ReactElement => { 14 | return ( 15 | 16 |

React Final Form - Simple Example

17 | 18 | Read Docs 19 | 20 |
( 24 | 25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 | 45 | 46 | 47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 |
61 | 62 |
63 | 66 | 69 | 72 | 75 |
76 |
77 |
78 | 79 |
80 | 83 | 86 | 89 |
90 |
91 |
92 | 93 | 94 |
95 |
96 | 99 | 102 |
103 |
{JSON.stringify(values, null, 2)}
104 |
105 | )} 106 | /> 107 |
108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | # This file was generated based on ".graphqlconfig". Do not edit manually. 2 | 3 | schema { 4 | query: query_root 5 | mutation: mutation_root 6 | subscription: subscription_root 7 | } 8 | 9 | "mutation root" 10 | type mutation_root { 11 | "delete data from the table: \"todo\"" 12 | delete_todo( 13 | "filter the rows which have to be deleted" 14 | where: todo_bool_exp! 15 | ): todo_mutation_response 16 | "delete single row from the table: \"todo\"" 17 | delete_todo_by_pk(id: Int!): todo 18 | "insert data into the table: \"todo\"" 19 | insert_todo( 20 | "the rows to be inserted" 21 | objects: [todo_insert_input!]!, 22 | "on conflict condition" 23 | on_conflict: todo_on_conflict 24 | ): todo_mutation_response 25 | "insert a single row into the table: \"todo\"" 26 | insert_todo_one( 27 | "the row to be inserted" 28 | object: todo_insert_input!, 29 | "on conflict condition" 30 | on_conflict: todo_on_conflict 31 | ): todo 32 | "update data of the table: \"todo\"" 33 | update_todo( 34 | "increments the numeric columns with given value of the filtered values" 35 | _inc: todo_inc_input, 36 | "sets the columns of the filtered rows to the given values" 37 | _set: todo_set_input, 38 | "filter the rows which have to be updated" 39 | where: todo_bool_exp! 40 | ): todo_mutation_response 41 | "update single row of the table: \"todo\"" 42 | update_todo_by_pk( 43 | "increments the numeric columns with given value of the filtered values" 44 | _inc: todo_inc_input, 45 | "sets the columns of the filtered rows to the given values" 46 | _set: todo_set_input, 47 | pk_columns: todo_pk_columns_input! 48 | ): todo 49 | } 50 | 51 | type query_root { 52 | "fetch data from the table: \"todo\"" 53 | todo( 54 | "distinct select on columns" 55 | distinct_on: [todo_select_column!], 56 | "limit the number of rows returned" 57 | limit: Int, 58 | "skip the first n rows. Use only with order_by" 59 | offset: Int, 60 | "sort the rows by one or more columns" 61 | order_by: [todo_order_by!], 62 | "filter the rows returned" 63 | where: todo_bool_exp 64 | ): [todo!]! 65 | "fetch aggregated fields from the table: \"todo\"" 66 | todo_aggregate( 67 | "distinct select on columns" 68 | distinct_on: [todo_select_column!], 69 | "limit the number of rows returned" 70 | limit: Int, 71 | "skip the first n rows. Use only with order_by" 72 | offset: Int, 73 | "sort the rows by one or more columns" 74 | order_by: [todo_order_by!], 75 | "filter the rows returned" 76 | where: todo_bool_exp 77 | ): todo_aggregate! 78 | "fetch data from the table: \"todo\" using primary key columns" 79 | todo_by_pk(id: Int!): todo 80 | } 81 | 82 | type subscription_root { 83 | "fetch data from the table: \"todo\"" 84 | todo( 85 | "distinct select on columns" 86 | distinct_on: [todo_select_column!], 87 | "limit the number of rows returned" 88 | limit: Int, 89 | "skip the first n rows. Use only with order_by" 90 | offset: Int, 91 | "sort the rows by one or more columns" 92 | order_by: [todo_order_by!], 93 | "filter the rows returned" 94 | where: todo_bool_exp 95 | ): [todo!]! 96 | "fetch aggregated fields from the table: \"todo\"" 97 | todo_aggregate( 98 | "distinct select on columns" 99 | distinct_on: [todo_select_column!], 100 | "limit the number of rows returned" 101 | limit: Int, 102 | "skip the first n rows. Use only with order_by" 103 | offset: Int, 104 | "sort the rows by one or more columns" 105 | order_by: [todo_order_by!], 106 | "filter the rows returned" 107 | where: todo_bool_exp 108 | ): todo_aggregate! 109 | "fetch data from the table: \"todo\" using primary key columns" 110 | todo_by_pk(id: Int!): todo 111 | } 112 | 113 | "columns and relationships of \"todo\"" 114 | type todo { 115 | description: String! 116 | done: Boolean 117 | id: Int! 118 | } 119 | 120 | "aggregated selection of \"todo\"" 121 | type todo_aggregate { 122 | aggregate: todo_aggregate_fields 123 | nodes: [todo!]! 124 | } 125 | 126 | "aggregate fields of \"todo\"" 127 | type todo_aggregate_fields { 128 | avg: todo_avg_fields 129 | count(columns: [todo_select_column!], distinct: Boolean): Int! 130 | max: todo_max_fields 131 | min: todo_min_fields 132 | stddev: todo_stddev_fields 133 | stddev_pop: todo_stddev_pop_fields 134 | stddev_samp: todo_stddev_samp_fields 135 | sum: todo_sum_fields 136 | var_pop: todo_var_pop_fields 137 | var_samp: todo_var_samp_fields 138 | variance: todo_variance_fields 139 | } 140 | 141 | "aggregate avg on columns" 142 | type todo_avg_fields { 143 | id: Float 144 | } 145 | 146 | "aggregate max on columns" 147 | type todo_max_fields { 148 | description: String 149 | id: Int 150 | } 151 | 152 | "aggregate min on columns" 153 | type todo_min_fields { 154 | description: String 155 | id: Int 156 | } 157 | 158 | "response of any mutation on the table \"todo\"" 159 | type todo_mutation_response { 160 | "number of rows affected by the mutation" 161 | affected_rows: Int! 162 | "data from the rows affected by the mutation" 163 | returning: [todo!]! 164 | } 165 | 166 | "aggregate stddev on columns" 167 | type todo_stddev_fields { 168 | id: Float 169 | } 170 | 171 | "aggregate stddev_pop on columns" 172 | type todo_stddev_pop_fields { 173 | id: Float 174 | } 175 | 176 | "aggregate stddev_samp on columns" 177 | type todo_stddev_samp_fields { 178 | id: Float 179 | } 180 | 181 | "aggregate sum on columns" 182 | type todo_sum_fields { 183 | id: Int 184 | } 185 | 186 | "aggregate var_pop on columns" 187 | type todo_var_pop_fields { 188 | id: Float 189 | } 190 | 191 | "aggregate var_samp on columns" 192 | type todo_var_samp_fields { 193 | id: Float 194 | } 195 | 196 | "aggregate variance on columns" 197 | type todo_variance_fields { 198 | id: Float 199 | } 200 | 201 | "column ordering options" 202 | enum order_by { 203 | "in ascending order, nulls last" 204 | asc 205 | "in ascending order, nulls first" 206 | asc_nulls_first 207 | "in ascending order, nulls last" 208 | asc_nulls_last 209 | "in descending order, nulls first" 210 | desc 211 | "in descending order, nulls first" 212 | desc_nulls_first 213 | "in descending order, nulls last" 214 | desc_nulls_last 215 | } 216 | 217 | "unique or primary key constraints on table \"todo\"" 218 | enum todo_constraint { 219 | "unique or primary key constraint" 220 | todo_pkey 221 | } 222 | 223 | "select columns of table \"todo\"" 224 | enum todo_select_column { 225 | "column name" 226 | description 227 | "column name" 228 | done 229 | "column name" 230 | id 231 | } 232 | 233 | "update columns of table \"todo\"" 234 | enum todo_update_column { 235 | "column name" 236 | description 237 | "column name" 238 | done 239 | "column name" 240 | id 241 | } 242 | 243 | "Boolean expression to compare columns of type \"Boolean\". All fields are combined with logical 'AND'." 244 | input Boolean_comparison_exp { 245 | _eq: Boolean 246 | _gt: Boolean 247 | _gte: Boolean 248 | _in: [Boolean!] 249 | _is_null: Boolean 250 | _lt: Boolean 251 | _lte: Boolean 252 | _neq: Boolean 253 | _nin: [Boolean!] 254 | } 255 | 256 | "Boolean expression to compare columns of type \"Int\". All fields are combined with logical 'AND'." 257 | input Int_comparison_exp { 258 | _eq: Int 259 | _gt: Int 260 | _gte: Int 261 | _in: [Int!] 262 | _is_null: Boolean 263 | _lt: Int 264 | _lte: Int 265 | _neq: Int 266 | _nin: [Int!] 267 | } 268 | 269 | "Boolean expression to compare columns of type \"String\". All fields are combined with logical 'AND'." 270 | input String_comparison_exp { 271 | _eq: String 272 | _gt: String 273 | _gte: String 274 | "does the column match the given case-insensitive pattern" 275 | _ilike: String 276 | _in: [String!] 277 | "does the column match the given POSIX regular expression, case insensitive" 278 | _iregex: String 279 | _is_null: Boolean 280 | "does the column match the given pattern" 281 | _like: String 282 | _lt: String 283 | _lte: String 284 | _neq: String 285 | "does the column NOT match the given case-insensitive pattern" 286 | _nilike: String 287 | _nin: [String!] 288 | "does the column NOT match the given POSIX regular expression, case insensitive" 289 | _niregex: String 290 | "does the column NOT match the given pattern" 291 | _nlike: String 292 | "does the column NOT match the given POSIX regular expression, case sensitive" 293 | _nregex: String 294 | "does the column NOT match the given SQL regular expression" 295 | _nsimilar: String 296 | "does the column match the given POSIX regular expression, case sensitive" 297 | _regex: String 298 | "does the column match the given SQL regular expression" 299 | _similar: String 300 | } 301 | 302 | "Boolean expression to filter rows from the table \"todo\". All fields are combined with a logical 'AND'." 303 | input todo_bool_exp { 304 | _and: [todo_bool_exp!] 305 | _not: todo_bool_exp 306 | _or: [todo_bool_exp!] 307 | description: String_comparison_exp 308 | done: Boolean_comparison_exp 309 | id: Int_comparison_exp 310 | } 311 | 312 | "input type for incrementing numeric columns in table \"todo\"" 313 | input todo_inc_input { 314 | id: Int 315 | } 316 | 317 | "input type for inserting data into table \"todo\"" 318 | input todo_insert_input { 319 | description: String 320 | done: Boolean 321 | id: Int 322 | } 323 | 324 | "on conflict condition type for table \"todo\"" 325 | input todo_on_conflict { 326 | constraint: todo_constraint! 327 | update_columns: [todo_update_column!]! = [] 328 | where: todo_bool_exp 329 | } 330 | 331 | "Ordering options when selecting data from \"todo\"." 332 | input todo_order_by { 333 | description: order_by 334 | done: order_by 335 | id: order_by 336 | } 337 | 338 | "primary key columns input for table: todo" 339 | input todo_pk_columns_input { 340 | id: Int! 341 | } 342 | 343 | "input type for updating data in table \"todo\"" 344 | input todo_set_input { 345 | description: String 346 | done: Boolean 347 | id: Int 348 | } 349 | -------------------------------------------------------------------------------- /src/@types/gen/graphql.d.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null 2 | export type Exact = { [K in keyof T]: T[K] } 3 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe } 4 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe } 5 | /** All built-in and custom scalars, mapped to their actual values */ 6 | export type Scalars = { 7 | ID: string 8 | String: string 9 | Boolean: boolean 10 | Int: number 11 | Float: number 12 | } 13 | 14 | /** Boolean expression to compare columns of type "Boolean". All fields are combined with logical 'AND'. */ 15 | export type Boolean_Comparison_Exp = { 16 | _eq?: Maybe 17 | _gt?: Maybe 18 | _gte?: Maybe 19 | _in?: Maybe> 20 | _is_null?: Maybe 21 | _lt?: Maybe 22 | _lte?: Maybe 23 | _neq?: Maybe 24 | _nin?: Maybe> 25 | } 26 | 27 | /** Boolean expression to compare columns of type "Int". All fields are combined with logical 'AND'. */ 28 | export type Int_Comparison_Exp = { 29 | _eq?: Maybe 30 | _gt?: Maybe 31 | _gte?: Maybe 32 | _in?: Maybe> 33 | _is_null?: Maybe 34 | _lt?: Maybe 35 | _lte?: Maybe 36 | _neq?: Maybe 37 | _nin?: Maybe> 38 | } 39 | 40 | /** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */ 41 | export type String_Comparison_Exp = { 42 | _eq?: Maybe 43 | _gt?: Maybe 44 | _gte?: Maybe 45 | /** does the column match the given case-insensitive pattern */ 46 | _ilike?: Maybe 47 | _in?: Maybe> 48 | /** does the column match the given POSIX regular expression, case insensitive */ 49 | _iregex?: Maybe 50 | _is_null?: Maybe 51 | /** does the column match the given pattern */ 52 | _like?: Maybe 53 | _lt?: Maybe 54 | _lte?: Maybe 55 | _neq?: Maybe 56 | /** does the column NOT match the given case-insensitive pattern */ 57 | _nilike?: Maybe 58 | _nin?: Maybe> 59 | /** does the column NOT match the given POSIX regular expression, case insensitive */ 60 | _niregex?: Maybe 61 | /** does the column NOT match the given pattern */ 62 | _nlike?: Maybe 63 | /** does the column NOT match the given POSIX regular expression, case sensitive */ 64 | _nregex?: Maybe 65 | /** does the column NOT match the given SQL regular expression */ 66 | _nsimilar?: Maybe 67 | /** does the column match the given POSIX regular expression, case sensitive */ 68 | _regex?: Maybe 69 | /** does the column match the given SQL regular expression */ 70 | _similar?: Maybe 71 | } 72 | 73 | /** mutation root */ 74 | export type Mutation_Root = { 75 | __typename?: 'mutation_root' 76 | /** delete data from the table: "todo" */ 77 | delete_todo?: Maybe 78 | /** delete single row from the table: "todo" */ 79 | delete_todo_by_pk?: Maybe 80 | /** insert data into the table: "todo" */ 81 | insert_todo?: Maybe 82 | /** insert a single row into the table: "todo" */ 83 | insert_todo_one?: Maybe 84 | /** update data of the table: "todo" */ 85 | update_todo?: Maybe 86 | /** update single row of the table: "todo" */ 87 | update_todo_by_pk?: Maybe 88 | } 89 | 90 | /** mutation root */ 91 | export type Mutation_RootDelete_TodoArgs = { 92 | where: Todo_Bool_Exp 93 | } 94 | 95 | /** mutation root */ 96 | export type Mutation_RootDelete_Todo_By_PkArgs = { 97 | id: Scalars['Int'] 98 | } 99 | 100 | /** mutation root */ 101 | export type Mutation_RootInsert_TodoArgs = { 102 | objects: Array 103 | on_conflict?: Maybe 104 | } 105 | 106 | /** mutation root */ 107 | export type Mutation_RootInsert_Todo_OneArgs = { 108 | object: Todo_Insert_Input 109 | on_conflict?: Maybe 110 | } 111 | 112 | /** mutation root */ 113 | export type Mutation_RootUpdate_TodoArgs = { 114 | _inc?: Maybe 115 | _set?: Maybe 116 | where: Todo_Bool_Exp 117 | } 118 | 119 | /** mutation root */ 120 | export type Mutation_RootUpdate_Todo_By_PkArgs = { 121 | _inc?: Maybe 122 | _set?: Maybe 123 | pk_columns: Todo_Pk_Columns_Input 124 | } 125 | 126 | /** column ordering options */ 127 | export enum Order_By { 128 | /** in ascending order, nulls last */ 129 | Asc = 'asc', 130 | /** in ascending order, nulls first */ 131 | AscNullsFirst = 'asc_nulls_first', 132 | /** in ascending order, nulls last */ 133 | AscNullsLast = 'asc_nulls_last', 134 | /** in descending order, nulls first */ 135 | Desc = 'desc', 136 | /** in descending order, nulls first */ 137 | DescNullsFirst = 'desc_nulls_first', 138 | /** in descending order, nulls last */ 139 | DescNullsLast = 'desc_nulls_last', 140 | } 141 | 142 | export type Query_Root = { 143 | __typename?: 'query_root' 144 | /** fetch data from the table: "todo" */ 145 | todo: Array 146 | /** fetch aggregated fields from the table: "todo" */ 147 | todo_aggregate: Todo_Aggregate 148 | /** fetch data from the table: "todo" using primary key columns */ 149 | todo_by_pk?: Maybe 150 | } 151 | 152 | export type Query_RootTodoArgs = { 153 | distinct_on?: Maybe> 154 | limit?: Maybe 155 | offset?: Maybe 156 | order_by?: Maybe> 157 | where?: Maybe 158 | } 159 | 160 | export type Query_RootTodo_AggregateArgs = { 161 | distinct_on?: Maybe> 162 | limit?: Maybe 163 | offset?: Maybe 164 | order_by?: Maybe> 165 | where?: Maybe 166 | } 167 | 168 | export type Query_RootTodo_By_PkArgs = { 169 | id: Scalars['Int'] 170 | } 171 | 172 | export type Subscription_Root = { 173 | __typename?: 'subscription_root' 174 | /** fetch data from the table: "todo" */ 175 | todo: Array 176 | /** fetch aggregated fields from the table: "todo" */ 177 | todo_aggregate: Todo_Aggregate 178 | /** fetch data from the table: "todo" using primary key columns */ 179 | todo_by_pk?: Maybe 180 | } 181 | 182 | export type Subscription_RootTodoArgs = { 183 | distinct_on?: Maybe> 184 | limit?: Maybe 185 | offset?: Maybe 186 | order_by?: Maybe> 187 | where?: Maybe 188 | } 189 | 190 | export type Subscription_RootTodo_AggregateArgs = { 191 | distinct_on?: Maybe> 192 | limit?: Maybe 193 | offset?: Maybe 194 | order_by?: Maybe> 195 | where?: Maybe 196 | } 197 | 198 | export type Subscription_RootTodo_By_PkArgs = { 199 | id: Scalars['Int'] 200 | } 201 | 202 | /** columns and relationships of "todo" */ 203 | export type Todo = { 204 | __typename?: 'todo' 205 | description: Scalars['String'] 206 | done?: Maybe 207 | id: Scalars['Int'] 208 | } 209 | 210 | /** aggregated selection of "todo" */ 211 | export type Todo_Aggregate = { 212 | __typename?: 'todo_aggregate' 213 | aggregate?: Maybe 214 | nodes: Array 215 | } 216 | 217 | /** aggregate fields of "todo" */ 218 | export type Todo_Aggregate_Fields = { 219 | __typename?: 'todo_aggregate_fields' 220 | avg?: Maybe 221 | count: Scalars['Int'] 222 | max?: Maybe 223 | min?: Maybe 224 | stddev?: Maybe 225 | stddev_pop?: Maybe 226 | stddev_samp?: Maybe 227 | sum?: Maybe 228 | var_pop?: Maybe 229 | var_samp?: Maybe 230 | variance?: Maybe 231 | } 232 | 233 | /** aggregate fields of "todo" */ 234 | export type Todo_Aggregate_FieldsCountArgs = { 235 | columns?: Maybe> 236 | distinct?: Maybe 237 | } 238 | 239 | /** aggregate avg on columns */ 240 | export type Todo_Avg_Fields = { 241 | __typename?: 'todo_avg_fields' 242 | id?: Maybe 243 | } 244 | 245 | /** Boolean expression to filter rows from the table "todo". All fields are combined with a logical 'AND'. */ 246 | export type Todo_Bool_Exp = { 247 | _and?: Maybe> 248 | _not?: Maybe 249 | _or?: Maybe> 250 | description?: Maybe 251 | done?: Maybe 252 | id?: Maybe 253 | } 254 | 255 | /** unique or primary key constraints on table "todo" */ 256 | export enum Todo_Constraint { 257 | /** unique or primary key constraint */ 258 | TodoPkey = 'todo_pkey', 259 | } 260 | 261 | /** input type for incrementing numeric columns in table "todo" */ 262 | export type Todo_Inc_Input = { 263 | id?: Maybe 264 | } 265 | 266 | /** input type for inserting data into table "todo" */ 267 | export type Todo_Insert_Input = { 268 | description?: Maybe 269 | done?: Maybe 270 | id?: Maybe 271 | } 272 | 273 | /** aggregate max on columns */ 274 | export type Todo_Max_Fields = { 275 | __typename?: 'todo_max_fields' 276 | description?: Maybe 277 | id?: Maybe 278 | } 279 | 280 | /** aggregate min on columns */ 281 | export type Todo_Min_Fields = { 282 | __typename?: 'todo_min_fields' 283 | description?: Maybe 284 | id?: Maybe 285 | } 286 | 287 | /** response of any mutation on the table "todo" */ 288 | export type Todo_Mutation_Response = { 289 | __typename?: 'todo_mutation_response' 290 | /** number of rows affected by the mutation */ 291 | affected_rows: Scalars['Int'] 292 | /** data from the rows affected by the mutation */ 293 | returning: Array 294 | } 295 | 296 | /** on conflict condition type for table "todo" */ 297 | export type Todo_On_Conflict = { 298 | constraint: Todo_Constraint 299 | update_columns?: Array 300 | where?: Maybe 301 | } 302 | 303 | /** Ordering options when selecting data from "todo". */ 304 | export type Todo_Order_By = { 305 | description?: Maybe 306 | done?: Maybe 307 | id?: Maybe 308 | } 309 | 310 | /** primary key columns input for table: todo */ 311 | export type Todo_Pk_Columns_Input = { 312 | id: Scalars['Int'] 313 | } 314 | 315 | /** select columns of table "todo" */ 316 | export enum Todo_Select_Column { 317 | /** column name */ 318 | Description = 'description', 319 | /** column name */ 320 | Done = 'done', 321 | /** column name */ 322 | Id = 'id', 323 | } 324 | 325 | /** input type for updating data in table "todo" */ 326 | export type Todo_Set_Input = { 327 | description?: Maybe 328 | done?: Maybe 329 | id?: Maybe 330 | } 331 | 332 | /** aggregate stddev on columns */ 333 | export type Todo_Stddev_Fields = { 334 | __typename?: 'todo_stddev_fields' 335 | id?: Maybe 336 | } 337 | 338 | /** aggregate stddev_pop on columns */ 339 | export type Todo_Stddev_Pop_Fields = { 340 | __typename?: 'todo_stddev_pop_fields' 341 | id?: Maybe 342 | } 343 | 344 | /** aggregate stddev_samp on columns */ 345 | export type Todo_Stddev_Samp_Fields = { 346 | __typename?: 'todo_stddev_samp_fields' 347 | id?: Maybe 348 | } 349 | 350 | /** aggregate sum on columns */ 351 | export type Todo_Sum_Fields = { 352 | __typename?: 'todo_sum_fields' 353 | id?: Maybe 354 | } 355 | 356 | /** update columns of table "todo" */ 357 | export enum Todo_Update_Column { 358 | /** column name */ 359 | Description = 'description', 360 | /** column name */ 361 | Done = 'done', 362 | /** column name */ 363 | Id = 'id', 364 | } 365 | 366 | /** aggregate var_pop on columns */ 367 | export type Todo_Var_Pop_Fields = { 368 | __typename?: 'todo_var_pop_fields' 369 | id?: Maybe 370 | } 371 | 372 | /** aggregate var_samp on columns */ 373 | export type Todo_Var_Samp_Fields = { 374 | __typename?: 'todo_var_samp_fields' 375 | id?: Maybe 376 | } 377 | 378 | /** aggregate variance on columns */ 379 | export type Todo_Variance_Fields = { 380 | __typename?: 'todo_variance_fields' 381 | id?: Maybe 382 | } 383 | --------------------------------------------------------------------------------