From fa35bdb992d753bf29b37615cade45f26ad84f47 Mon Sep 17 00:00:00 2001 From: honzapatCZ Date: Thu, 17 Oct 2024 23:17:27 +0200 Subject: [PATCH] init --- Flag.tsx | 12 ++++ Form/Core/AutocompleteSelect.js | 100 ++++++++++++++++++++++++++++++++ Form/Core/DateFields.js | 22 +++++++ Form/Core/DateTimeField.js | 32 ++++++++++ Form/Core/FileField.js | 31 ++++++++++ Form/Core/NumberField.js | 8 +++ Form/FormikForm.tsx | 49 ++++++++++++++++ Form/withForm.tsx | 35 +++++++++++ Table/NejReactTable.tsx | 30 ++++++++++ Table/useNejReactTable.ts | 15 +++++ globalStyle.js | 75 ++++++++++++++++++++++++ styledRegistry.js | 25 ++++++++ useModal.tsx | 16 +++++ 13 files changed, 450 insertions(+) create mode 100644 Flag.tsx create mode 100644 Form/Core/AutocompleteSelect.js create mode 100644 Form/Core/DateFields.js create mode 100644 Form/Core/DateTimeField.js create mode 100644 Form/Core/FileField.js create mode 100644 Form/Core/NumberField.js create mode 100644 Form/FormikForm.tsx create mode 100644 Form/withForm.tsx create mode 100644 Table/NejReactTable.tsx create mode 100644 Table/useNejReactTable.ts create mode 100644 globalStyle.js create mode 100644 styledRegistry.js create mode 100644 useModal.tsx diff --git a/Flag.tsx b/Flag.tsx new file mode 100644 index 0000000..799775c --- /dev/null +++ b/Flag.tsx @@ -0,0 +1,12 @@ +import { Country } from "@services/accounting-api"; +import { lazy, Suspense } from 'react'; + +export default function Flag({ country, ...props }: { country: Country | "EU" }) { + const FlagIcon = lazy(() => import(`country-flag-icons/react/3x2`).then(module => ({ default: module[country] }))); + + return ( + ...}> + + + ); +} \ No newline at end of file diff --git a/Form/Core/AutocompleteSelect.js b/Form/Core/AutocompleteSelect.js new file mode 100644 index 0000000..d8cff35 --- /dev/null +++ b/Form/Core/AutocompleteSelect.js @@ -0,0 +1,100 @@ +import { Combobox } from "@headlessui/react"; + +import { DropDownItem } from "@shared/nej-react-components/Parts/DropDown"; +import { useEffect, useState } from "react"; + +import tw, { styled } from "twin.macro"; +import "styled-components/macro"; +import { useField } from "@shared/nej-react-components/Parts/Input"; + +export function AutocompleteSelect({ title, titleProps = null, data, nullable = false, ...props }) { + const [field, meta, helpers] = useField(props); + + const [query, setQuery] = useState(field.value ?? ""); + const [internalData, setInternalData] = useState([]); + + useEffect(() => { + (async () => { + if (typeof data === "function") { + //if data is async function await it + setInternalData(await data(query)); + } else if (Array.isArray(data)) { + setInternalData( + query === "" + ? data + : data.filter((item) => { + if (typeof item === "string") + return item.toLowerCase().includes(query.toLowerCase()); + + return item.label.toLowerCase().includes(query.toLowerCase()); + }) + ); + } else { + console.error("data is not an array or function :c"); + setInternalData([]); + } + })(); + }, [query, data]); + + return ( + <> + + typeof item === "string" + ? item == field.value + : item.value === field.value + )[0] + : null + } + onChange={(value) => { + console.log(value); + field.onChange({ target: { value: value, name: field.name } }); + }} + nullable={nullable} + > +
+ {title && ( + + )} +
+ setQuery(event.target.value)} + displayValue={(val) => (typeof val === "string" ? val : val?.label)} + /> + +
+ + {internalData?.map((val) => ( + + + {typeof val === "string" ? val : val.label} + + + ))} + +
+
+ + {meta?.touched && meta.error ? ( +
{meta.error}
+ ) : null} + + ); +} diff --git a/Form/Core/DateFields.js b/Form/Core/DateFields.js new file mode 100644 index 0000000..8e88407 --- /dev/null +++ b/Form/Core/DateFields.js @@ -0,0 +1,22 @@ +import tw, { styled } from "twin.macro"; +import "styled-components/macro"; + +import Input, { useField } from "@shared/nej-react-components/Parts/Input"; + +export default function DateField(props) { + + const [{onChange, value, ...field}, meta, helpers] = useField(props); + + let realDate = (typeof(value) === "string" ? new Date(Date.parse(value)) : value); + if(typeof(realDate) != "object") + realDate = null; + //check if its ivnalid, if so make it null + if(isNaN(realDate)) + realDate = null; + + const {value: _x, onChange: _y, ...restProps} = props; + + return onChange(new Date(e.target.value))} {...field} {...restProps} />; +} diff --git a/Form/Core/DateTimeField.js b/Form/Core/DateTimeField.js new file mode 100644 index 0000000..bd398ba --- /dev/null +++ b/Form/Core/DateTimeField.js @@ -0,0 +1,32 @@ +import tw, { styled } from "twin.macro"; +import "styled-components/macro"; + +import Input, { useField } from "@shared/nej-react-components/Parts/Input"; + +export default function DateTimeField(props) { + + const [{onChange, value, ...field}, meta, helpers] = useField(props); + + //console.log(value) + /*** + * @type {Date} + */ + let realDate = (typeof(value) === "string" ? new Date(Date.parse( (value.length > 19 || value.length < 10) ? value : value + "Z")) : value); + if(typeof(realDate) != "object") + realDate = null; + //check if its ivnalid, if so make it null + if(isNaN(realDate)) + realDate = null; + + const {value: _x, onChange: _y, ...restProps} = props; + // console.log(realDate) + // console.log(realDate?.toISOString().slice(0,19)) + + return { + const value = e.target.value; + console.log( (value.length > 19 || value.length < 10) ? value : value + "Z") + onChange(new Date( (value.length > 19 || value.length < 10) ? value : value + "Z")) + }} {...field} {...restProps} />; +} diff --git a/Form/Core/FileField.js b/Form/Core/FileField.js new file mode 100644 index 0000000..46133dc --- /dev/null +++ b/Form/Core/FileField.js @@ -0,0 +1,31 @@ +import tw from "twin.macro"; +import { useDropzone } from "react-dropzone"; +import { useMemo } from "react"; +import { useApiClient, useCompany } from "@utils/NejManager/NejProvider"; +import axios from "axios"; +import { useField } from "formik"; +import NejDropzone from "@components/File/NejDropzone"; + +/** + * Renders a file input field with dropzone functionality. + * + * @param {Object} props - The component props. + * @param {boolean} props.single - Whether to allow only one file to be uploaded + * @param {string | string[]} props.accept - Array of supported file extensions. + * @returns {JSX.Element} The rendered file input field. + */ +export function FileField({ single, accept, ...props }) { + const [field, meta, helpers] = useField(props); + + /** + * Handles the upload of files. + * + * @param {Array} files - The files to be uploaded. + */ + function onUpload(files) { + console.log(files) + field.onChange({ target: { value: single ? files[0] : files, name: field.name } }); + } + + return ; +} diff --git a/Form/Core/NumberField.js b/Form/Core/NumberField.js new file mode 100644 index 0000000..0102985 --- /dev/null +++ b/Form/Core/NumberField.js @@ -0,0 +1,8 @@ +import tw, { styled } from "twin.macro"; +import "styled-components/macro"; + +import Input from "@shared/nej-react-components/Parts/Input"; + +export default function NumberField(props) { + return ; +} diff --git a/Form/FormikForm.tsx b/Form/FormikForm.tsx new file mode 100644 index 0000000..9a19506 --- /dev/null +++ b/Form/FormikForm.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from "react-i18next"; +import { Form, Formik, FormikConfig, FormikErrors, FormikProps } from "formik"; +import { Divider } from "@shared/nej-react-components/Parts/Text"; +import Button from "@shared/nej-react-components/Parts/Button"; + +//formik is defined as +//export declare function Formik(props: FormikConfig & ExtraProps): React.JSX.Element; +//we just want to proxy it basically +export function FormikForm({ + data = {} as Values, + onSubmit, + submitText = "Submit", + schema, + onDelete = null, + closeModal = null, + createForm = null, + children, + ...props +}: Omit, "initialValues"> & { + data?: Values; + submitText?: string; + schema?: any; + onDelete?: () => void; + closeModal?: () => void; + createForm?: (values, handleChange, errors, touched) => React.ReactNode; + children?: React.ReactNode; +}): JSX.Element { + const { t } = useTranslation(); + + return + initialValues={data} + onSubmit={onSubmit} + validationSchema={schema} + {...props} + > + {({ values, handleChange, errors, touched, ...other }) => ( +
+ {createForm && createForm(values, handleChange, errors, touched)} + {children} + +
+ {onDelete && } + {closeModal && } + +
+ + )} + ; +} diff --git a/Form/withForm.tsx b/Form/withForm.tsx new file mode 100644 index 0000000..05bdffa --- /dev/null +++ b/Form/withForm.tsx @@ -0,0 +1,35 @@ +import { FormikForm } from "@shared/nej-react-utils/Form/FormikForm"; +import { NejAccountingApiClient, CompanyResponse, CancelablePromise } from "@services/accounting-api"; +import { useApiClient, useCompany } from "@utils/NejManager/NejProvider"; +import useMethod from "../../../utils/useMethod.macro"; +import useQuery from "../../../utils/useQuery.macro"; + +type FormProps = { + refresh: () => void; + initialData?: T; +}; +export type WithFormProps = P & FormProps; +export function withForm>(WrappedForm: React.ComponentType, method: (client: NejAccountingApiClient, company: CompanyResponse, props: P) => (body: T) => CancelablePromise, submitText: string) { + return function GameFormWrapper({ refresh, initialData, ...props }: P) { + const client = useApiClient(); + const company = useCompany(); + + const fnc = method(client, company, { ...props, initialData } as P); + console.log(fnc); + + const [submit, { }] = useMethod( + fnc, + () => refresh() + ); + + return ( + submit(values)} + submitText={submitText} + > + + + ); + }; +} diff --git a/Table/NejReactTable.tsx b/Table/NejReactTable.tsx new file mode 100644 index 0000000..621dbc8 --- /dev/null +++ b/Table/NejReactTable.tsx @@ -0,0 +1,30 @@ +import { MantineReactTable, MRT_RowData, MRT_TableInstance, MRT_TableOptions, useMantineReactTable, Xor } from "mantine-react-table"; +import { useNejReactTable } from "./useNejReactTable"; + +type TableInstanceProp = { + table: MRT_TableInstance; +}; + +type Props = Xor< + TableInstanceProp, + MRT_TableOptions +>; + +const isTableInstanceProp = ( + props: Props, +): props is TableInstanceProp => + (props as TableInstanceProp).table !== undefined; + +export const NejReactTable = ( + props: Props, +) => { + let table: MRT_TableInstance; + + if (isTableInstanceProp(props)) { + table = props.table; + } else { + table = useNejReactTable(props); + } + + return ; +}; diff --git a/Table/useNejReactTable.ts b/Table/useNejReactTable.ts new file mode 100644 index 0000000..97af6b4 --- /dev/null +++ b/Table/useNejReactTable.ts @@ -0,0 +1,15 @@ +import { MRT_RowData, MRT_TableInstance, MRT_TableOptions, useMantineReactTable } from "mantine-react-table"; + + +export const useNejReactTable = ( + tableOptions: MRT_TableOptions, +): MRT_TableInstance => { + return useMantineReactTable({ + enablePagination: false, + enableFullScreenToggle: false, + enableDensityToggle: false, + enableBottomToolbar: false, + enableTopToolbar: tableOptions.renderTopToolbarCustomActions != null, + ...tableOptions, + }); +} \ No newline at end of file diff --git a/globalStyle.js b/globalStyle.js new file mode 100644 index 0000000..7852492 --- /dev/null +++ b/globalStyle.js @@ -0,0 +1,75 @@ +import { createGlobalStyle } from "styled-components" +import tw, { theme, GlobalStyles as BaseStyles } from "twin.macro" + +const CustomStyles = createGlobalStyle` +body { + -webkit-tap-highlight-color: ${theme`colors.accent`}; +} + +:root{ + --primary: #3d3d3d; + --secondary: #535353; + --trinary: #2c2c2c; + + --primary-text: #ffffff; + --secondary-text: #a0a0a0; + + --primary-invert: #f3f3f3; + --secondary-invert: #dbdbdb; + --trinary-invert: #ffffff; + + --primary-invert-text: #3d3d3d; + --secondary-invert-text: #535353; + + --accent: #00C800; + --accent-dark: #008f00; + --accent-light: #00ed00; + --accent2: #3080FF; + --accent2-dark: #225ab4; + --accent3: #804000; + --accent3-dark: #472400; + --accent4: #F8B02C; + --accent4-dark: #bd8724; + --accent5: #9E3086; + --accent5-dark: #6b215b; +} + +.light\-theme{ + --primary: #f3f3f3; + --secondary: #dbdbdb; + --trinary: #ffffff; + --primary-text: #3d3d3d; + --secondary-text: #535353; + + + --primary-invert: #3d3d3d; + --secondary-invert: #535353; + --trinary-invert: #2c2c2c; + --primary-text-invert: #ffffff; + --secondary-text-invert: #a0a0a0; +} + + +::-webkit-scrollbar { + width: 0.5rem; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--secondary); + border-radius: 0.25rem; +} +::-webkit-scrollbar-thumb:hover { + background: var(--accent-dark); +} +` + +const GlobalStyles = ({dontApplyBaseStyles}) => { + return <> + {(!dontApplyBaseStyles) &&} + + +} + +export default GlobalStyles \ No newline at end of file diff --git a/styledRegistry.js b/styledRegistry.js new file mode 100644 index 0000000..10da128 --- /dev/null +++ b/styledRegistry.js @@ -0,0 +1,25 @@ +'use client' + +import React, { useState } from 'react' +import { useServerInsertedHTML } from 'next/navigation' +import { ServerStyleSheet, StyleSheetManager } from 'styled-components' + +export default function StyledComponentsRegistry({ children }) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()) + + useServerInsertedHTML(() => { + const styles = styledComponentsStyleSheet.getStyleElement() + styledComponentsStyleSheet.instance.clearTag() + return <>{styles} + }) + + if (typeof window !== 'undefined') return <>{children} + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/useModal.tsx b/useModal.tsx new file mode 100644 index 0000000..a970221 --- /dev/null +++ b/useModal.tsx @@ -0,0 +1,16 @@ +import { modals } from "@mantine/modals"; +import { useCallback } from "react"; + +export default function useModal( + Component: React.ComponentType, + title: string + ) { + const openModal = useCallback((params: T) => { + modals.open({ + title, + children: , + }); + }, [Component, title]); + + return openModal; +} \ No newline at end of file