Merge pull request #2 from internet4refugees/feature/search-frontend

next project scaffold + table for filtering and search
This commit is contained in:
Johannes Lötzsch 2022-03-09 17:21:58 +01:00 committed by GitHub
commit c4955c067d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 5849 additions and 10428 deletions

View File

@ -9,7 +9,7 @@
:place_city "",
:contact_phone nil,
:place_zip "",
:time_from_str "",
:time_from_str "10/04/2022",
:place_country "",
:animals_present false,
:languages (),
@ -26,7 +26,7 @@
:place_city "",
:contact_phone nil,
:place_zip "",
:time_from_str "",
:time_from_str "03/04/2022",
:place_country nil,
:animals_present true,
:languages (),
@ -43,7 +43,7 @@
:place_city nil,
:contact_phone "Q",
:place_zip "RD",
:time_from_str "q",
:time_from_str "03/04/2022",
:place_country nil,
:animals_present true,
:languages (),
@ -60,7 +60,7 @@
:place_city "1bh",
:contact_phone nil,
:place_zip "PG4",
:time_from_str "8az",
:time_from_str "03/04/2022",
:place_country "I7",
:animals_present false,
:languages ("x" "ni"),
@ -77,7 +77,7 @@
:place_city "x",
:contact_phone "w8l",
:place_zip "1E",
:time_from_str "6",
:time_from_str "01/01/2022",
:place_country "8",
:animals_present nil,
:languages (),
@ -94,7 +94,7 @@
:place_city "6",
:contact_phone "OI",
:place_zip "1",
:time_from_str "hS192",
:time_from_str "03/04/2022",
:place_country "",
:animals_present false,
:languages ("DX" "E" "G3wuv"),
@ -111,7 +111,7 @@
:place_city "cj1A2",
:contact_phone "r",
:place_zip "cJ9z7",
:time_from_str "",
:time_from_str "03/04/2022",
:place_country "5",
:animals_present true,
:languages nil,
@ -128,7 +128,7 @@
:place_city "4ML4yG",
:contact_phone "7uQ3Px",
:place_zip "kp5IS",
:time_from_str "Y5lSe",
:time_from_str "03/04/2022",
:place_country nil,
:animals_present true,
:languages (),
@ -145,7 +145,7 @@
:place_city "u6k41v",
:contact_phone "354VW",
:place_zip "DI19",
:time_from_str "Z0QGzI8",
:time_from_str "09/04/2022",
:place_country nil,
:animals_present true,
:languages nil,
@ -162,7 +162,7 @@
:place_city "f",
:contact_phone "sQdB",
:place_zip "cid1",
:time_from_str "KSAFC",
:time_from_str "03/05/2022",
:place_country "toTz5B",
:animals_present false,
:languages ("vMR0zyECr"),

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

View File

@ -1 +1,38 @@
node_modules
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo

View File

@ -1,3 +1,20 @@
TODO: A frontend for authorized NGO members, to search the database
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
* [The submisison frontend](../submission/README.md) should be written first (since it is more simple / closer to the hello-world of jsonforms)

View File

@ -0,0 +1,14 @@
import * as React from 'react';
import {QueryClient, QueryClientProvider} from 'react-query'
import {ReactQueryDevtools} from 'react-query/devtools'
const queryClient = new QueryClient()
export default function MyQueryClientProvider({children}: {children: React.Component}) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

View File

@ -0,0 +1,155 @@
import React from 'react'
import '@inovua/reactdatagrid-community/index.css'
import DataGrid from '@inovua/reactdatagrid-community'
import DateFilter from '@inovua/reactdatagrid-community/DateFilter'
import StringFilter from '@inovua/reactdatagrid-community/StringFilter'
import BoolFilter from '@inovua/reactdatagrid-community/BoolFilter'
import {GetOffersQuery} from "../../codegen/generates";
import {TypeColumn, TypeFilterValue, TypeSingleFilterValue} from "@inovua/reactdatagrid-community/types";
import NumberFilter from "@inovua/reactdatagrid-community/NumberFilter";
import moment from "moment";
global.moment = moment
type HostOfferLookupTableProps = {
data: GetOffersQuery
}
const columnsRaw: { name: string; header: string; type: string }[] = [
{
"name": "beds",
"header": "beds",
"type": "number"
},
{
"name": "place_street",
"header": "place street",
"type": "string"
},
{
"name": "contact_email",
"header": "contact email",
"type": "object"
},
{
"name": "contact_name_full",
"header": "contact name full",
"type": "string"
},
{
"name": "time_duration_str",
"header": "time duration str",
"type": "string"
},
{
"name": "note",
"header": "note",
"type": "string"
},
{
"name": "place_street_number",
"header": "place street number",
"type": "string"
},
{
"name": "place_city",
"header": "place city",
"type": "string"
},
{
"name": "contact_phone",
"header": "contact phone",
"type": "object"
},
{
"name": "place_zip",
"header": "place zip",
"type": "string"
},
{
"name": "time_from_str",
"header": "time from str",
"type": "date"
},
{
"name": "place_country",
"header": "place country",
"type": "string"
},
{
"name": "animals_present",
"header": "animals present",
"type": "boolean"
},
{
"name": "languages",
"header": "languages",
"type": "object"
},
{
"name": "accessible",
"header": "accessible",
"type": "boolean"
},
{
"name": "animals_allowed",
"header": "animals allowed",
"type": "boolean"
}
]
const filterMappings = {
string: StringFilter,
boolean: BoolFilter,
number: NumberFilter,
date: DateFilter
}
const operatorsForType = {
number: 'gte',
string: 'contains',
date: 'inrange'}
const columns: TypeColumn[] = columnsRaw
.map(c => ({
...c,
filterEditor: filterMappings[c.type as 'string' | 'number' | 'boolean' | 'date']
}))
const defaultFilterValue: TypeFilterValue = columns
.filter(( {type} ) => type && ['string', 'number', 'date'].includes( type ))
.map(( {name, type} ) => {
return {
name,
type,
value: '',
operator: operatorsForType[type as 'string' | 'number' | 'date']
} as unknown as TypeSingleFilterValue
} )
const makeColumnDefinition = (data: any ) => Object.keys(data)
.map(k => ({
name: k,
header: k.replace(/_/g, ' '),
type: typeof data[k]}))
const HostOfferLookupTable = ({ data }: HostOfferLookupTableProps) => {
const dataSource = data.get_offers || []
return <>
<DataGrid
idProperty="id"
filterable
showColumnMenuFilterOptions={true}
showFilteringMenuItems={true}
defaultFilterValue={defaultFilterValue}
rowIndexColumn
enableSelection
columns={columns}
dataSource={dataSource}
style={{minHeight: '1000px'}}
/>
</>
}
export default HostOfferLookupTable

View File

@ -0,0 +1,17 @@
import React from 'react'
import {Auth, useGetOffersQuery} from "../../codegen/generates";
import testAuth from '../util/testAuth.json'
import HostOfferLookupTable from "./HostOfferLookupTable";
type HostLookupWrapperProps = Record<string, never>
const HostOfferLookupWrapper = ({}: HostLookupWrapperProps) => {
const {data, isFetching} = useGetOffersQuery({auth: testAuth as Auth}, {staleTime: 60 * 1000})
if (isFetching) {
return "loading…"
} else if (data?.get_offers) {
return (<HostOfferLookupTable data={data}/>)
}
}
export default HostOfferLookupWrapper

View File

@ -0,0 +1,4 @@
{
"mail": "crewing@example-ngo.com",
"password": "Vr(+cFtUG=rsj2:/]*uR"
}

11
frontend/search/config.ts Normal file
View File

@ -0,0 +1,11 @@
import * as default_config from './public/config.json'
export let config = default_config /** to infer the types **/
let config_fetched = false
export async function fetch_config () {
if(!config_fetched) {
await fetch("/config.json").then(async res => {config = await res.json()
config_fetched = true})
}
}

View File

@ -0,0 +1,24 @@
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './en.json'
import de from './de.json'
export const resources = {
en: { translation: en },
de: { translation: de }
} as const
i18next
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources,
//lng: "en", /* we use LanguageDetector instead */
fallbackLng: "en",
interpolation: {
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
}
})

View File

@ -0,0 +1,2 @@
{
}

View File

@ -0,0 +1,2 @@
{
}

5
frontend/search/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,45 @@
{
"name": "beherbergung-search-frontend",
"version": "0.0.1",
"name": "beherbergung",
"version": "0.1.0",
"private": true,
"scripts": {
"generate": "cd codegen && graphql-codegen generate",
"dev": "next dev",
"build": "next build --no-lint",
"build": "next build",
"start": "next start",
"export": "next export"
"lint": "next lint"
},
"dependencies": {
"graphql-request": "^3.7.0",
"react-query": "^3.34.0"
"@emotion/react": "^11.8.1",
"@emotion/styled": "^11.8.1",
"@inovua/reactdatagrid-community": "^4.16.2",
"@jsonforms/core": "3.0.0-alpha.3",
"@jsonforms/material-renderers": "3.0.0-alpha.3",
"@jsonforms/react": "3.0.0-alpha.3",
"@mui/icons-material": "^5.2.0",
"@mui/lab": "^5.0.0-alpha.71",
"@mui/material": "^5.2.2",
"@mui/styles": "^5.2.3",
"graphql-request": "^4.0.0",
"i18next": "^21.6.13",
"i18next-browser-languagedetector": "^6.1.3",
"lodash": "^4.17.21",
"next": "12.1.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "^11.15.5",
"react-query": "^3.34.16"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.3.0",
"@graphql-codegen/typescript": "^2.4.1",
"@graphql-codegen/typescript-operations": "^2.2.1",
"@graphql-codegen/typescript-react-query": "^3.2.2",
"typescript": "4.5.2"
"@types/lodash": "^4.14.179",
"@types/node": "17.0.21",
"@types/react": "17.0.39",
"eslint": "8.10.0",
"eslint-config-next": "12.1.0",
"typescript": "4.6.2"
}
}

View File

@ -0,0 +1,11 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import MyQueryClientProvider from "../components/QueryClientProvider";
function MyApp({ Component, pageProps }: AppProps) {
return <MyQueryClientProvider>
<Component {...pageProps} />
</MyQueryClientProvider>
}
export default MyApp

View File

@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View File

@ -0,0 +1,22 @@
import type { NextPage } from 'next'
import Head from 'next/head'
import HostOfferLookupWrapper from '../components/ngo/HostOfferLookupWrapper'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
</Head>
<main className={styles.main}>
<HostOfferLookupWrapper />
</main>
</div>
)
}
export default Home

View File

@ -0,0 +1,2 @@
{"base_url": "http://localhost:3000",
"backend_base_url": "http://localhost:4000"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,8 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 4rem;
}

View File

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

5418
frontend/search/yarn.lock Normal file

File diff suppressed because it is too large Load Diff