Compare commits

..

No commits in common. "main" and "experimental/server-actions" have entirely different histories.

63 changed files with 6276 additions and 8475 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
USE_DEV_COLORS=
URL_IUS_DIRECT=

13
.gitignore vendored
View File

@ -1,14 +1,9 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Logs # dependencies
logs node_modules
*.log .pnp
npm-debug.log* .pnp.js
# Dependency directory
**/node_modules/**
_node_modules
.pnp.cjs
# testing # testing
coverage coverage

View File

@ -31,6 +31,5 @@
"tailwindCSS.experimental.classRegex": [ "tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
], ]
"editor.inlineSuggest.showToolbar": "always"
} }

View File

@ -18,9 +18,9 @@ This Turborepo includes the following packages/apps:
- `docs`: a [Next.js](https://nextjs.org/) app - `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app - `web`: another [Next.js](https://nextjs.org/) app
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications - `ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) - `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/tsconfig`: `tsconfig.json`s used throughout the monorepo - `tsconfig`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).

View File

@ -1,19 +1,16 @@
FROM node:alpine AS builder FROM node:alpine AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
ENV PNPM_HOME=/usr/local/bin
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
RUN pnpm add -g turbo RUN yarn global add turbo
RUN yarn global add dotenv-cli
COPY . . COPY . .
RUN turbo prune --scope=web --docker RUN turbo prune --scope=web --docker
# Add lockfile and package.json's of isolated subworkspace # Add lockfile and package.json's of isolated subworkspace
FROM node:alpine AS installer FROM node:alpine AS installer
RUN corepack enable && corepack prepare pnpm@latest --activate
ENV PNPM_HOME=/usr/local/bin
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
WORKDIR /app WORKDIR /app
@ -21,16 +18,14 @@ WORKDIR /app
# First install the dependencies (as they change less often) # First install the dependencies (as they change less often)
COPY .gitignore .gitignore COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml COPY --from=builder /app/out/yarn.lock ./yarn.lock
COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml RUN yarn install
RUN pnpm install
# Build the project # Build the project
COPY --from=builder /app/out/full/ . COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG URL_IUS_DIRECT COPY .env .env
ARG NEXT_PUBLIC_USE_DEV_COLORS RUN yarn dotenv -e .env turbo run build --filter=web...
RUN pnpm turbo run build --filter=web...
FROM node:alpine AS runner FROM node:alpine AS runner
WORKDIR /app WORKDIR /app

View File

@ -1,3 +1,4 @@
'use server';
import type * as t from './types'; import type * as t from './types';
import { getUrls } from '@/config/urls'; import { getUrls } from '@/config/urls';
import { createUrl, type PageUrlParams } from '@/utils/url'; import { createUrl, type PageUrlParams } from '@/utils/url';
@ -5,116 +6,55 @@ import type { WretchError } from 'wretch';
import wretch from 'wretch'; import wretch from 'wretch';
const urls = getUrls(); const urls = getUrls();
const api = wretch(urls.URL_IUS).options({ cache: 'no-store' }).errorType('json'); const api = wretch(urls.URL_UIS).errorType('json');
type Input = { cookie?: string; pageUrlParams: PageUrlParams; payload?: unknown }; type Input = { pageUrlParams: PageUrlParams; payload?: unknown };
export async function getData({ pageUrlParams, cookie = '' }: Input) { export async function getData({ pageUrlParams }: Input) {
const url = createUrl({ const url = createUrl({
...pageUrlParams, ...pageUrlParams,
route: '', route: '',
}); });
return api return api
.headers({ cookie })
.get(url) .get(url)
.res<t.ResponseGetData>((cb) => cb.json()) .res<t.ResponseGetData>((cb) => cb.json())
.then((res) => res); .then((res) => res);
} }
export async function getMetaData({ pageUrlParams, cookie = '' }: Input) { export async function getMetaData({ pageUrlParams }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/meta' }); const url = createUrl({ ...pageUrlParams, route: '/meta' });
return api return api
.headers({ cookie })
.get(url) .get(url)
.res<t.ResponseMetaData>((res) => res.json()) .res<t.ResponseMetaData>((res) => res.json())
.then((res) => res); .then((res) => res);
} }
export async function getConfig({ pageUrlParams, cookie = '' }: Input) { export async function getConfig({ pageUrlParams }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/config' }); const url = createUrl({ ...pageUrlParams, route: '/config' });
return api return api
.headers({ cookie })
.get(url) .get(url)
.res<t.ResponseConfig>((res) => res.json()) .res<t.ResponseConfig>((res) => res.json())
.then((res) => res); .then((res) => res);
} }
export async function getConditions({ pageUrlParams, cookie = '' }: Input) { export async function getConditions({ pageUrlParams }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/conditions' }); const url = createUrl({ ...pageUrlParams, route: '/conditions' });
return api return api
.headers({ cookie })
.get(url) .get(url)
.res<t.ResponseConditions>((res) => res.text()) .res<t.ResponseConditions>((res) => res.text())
.then((res) => res); .then((res) => res);
} }
export async function save({ pageUrlParams, payload }: Input) { export async function validate({ pageUrlParams, payload }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/transfer' }); const url = createUrl({ ...pageUrlParams, route: '/validate' });
return api return api
.post(payload, url) .post(payload, url)
.res<boolean>((res) => res.ok) .res<boolean>((res) => res.ok)
.then((res) => res) .then((res) => res)
.catch((error: WretchError) => error.json as t.HttpValidationError | t.HttpError); .catch((error: WretchError) => error.json as t.HttpValidationError);
}
export async function retract({ pageUrlParams, payload }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/return' });
return api
.post(payload, url)
.res<boolean>((res) => res.ok)
.then((res) => res)
.catch((error: WretchError) => error.json as t.HttpValidationError | t.HttpError);
}
export async function getDocumentTypes({ pageUrlParams, cookie = '' }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/documenttypes' });
return api
.headers({ cookie })
.get(url)
.res<t.ResponseDocumentTypes>((res) => res.json())
.then((res) => res);
}
export async function getDocuments({ pageUrlParams, cookie = '' }: Input) {
const url = createUrl({ ...pageUrlParams, route: '/documents' });
return api
.headers({ cookie })
.get(url)
.res<t.ResponseDocuments>((res) => res.json())
.then((res) => res);
}
export function uploadDocument({
pageUrlParams,
document,
formData,
}: Input & { document: Pick<t.DocumentType, 'documentTypeId'>; formData: FormData }) {
const url = createUrl({
...pageUrlParams,
route: '/document',
urlSearchParams: { ...pageUrlParams.urlSearchParams, ...document },
});
return fetch(urls.URL_IUS + url, {
body: formData,
method: 'POST',
})
.then((res) => {
if (res.ok) {
return true;
}
return res.json();
})
.catch((error) => {
throw error as t.HttpError;
});
} }

View File

@ -1,34 +0,0 @@
import type { Document, DocumentType } from './types';
export function combineDocuments({
documentTypes,
documents,
}: {
documentTypes: DocumentType[];
documents: Document[];
}) {
if (!documents.length) {
return documentTypes.map((x) => ({ ...(x as Document), canUpload: true }));
}
const nonUploadableDocuments = documents
.filter(
(document) =>
!documentTypes.some(
(documentType) => documentType.documentTypeId === document.documentTypeId
)
)
.map((document) => ({ ...document, canUpload: false }));
return documentTypes
.map((documentType) => {
const targetDocument = documents.find(
(document) => document.documentTypeId === documentType.documentTypeId
);
return { ...documentType, ...targetDocument, canUpload: true };
})
.concat(nonUploadableDocuments);
}
export type CombinedDocuments = ReturnType<typeof combineDocuments>;

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export type MetaObject = { export type MetaObject = {
disabled: boolean; disabled: boolean;
fieldType: 'CHECKBOX' | 'DECIMAL' | 'INT' | 'STRING' | 'BIGSTRING'; fieldType: 'CHECKBOX' | 'DECIMAL' | 'INT' | 'STRING';
label: string; label: string;
max: number; max: number;
min: number; min: number;
@ -11,22 +11,10 @@ export type MetaObject = {
type Value = any; type Value = any;
export type DocumentType = {
documentTypeId: string;
name: string;
};
export type Document = DocumentType & {
documentId?: string;
href?: string;
};
export type ResponseGetData = Record<string, Value>; export type ResponseGetData = Record<string, Value>;
export type ResponseMetaData = Record<string, MetaObject>; export type ResponseMetaData = Record<string, MetaObject>;
export type ResponseConfig = { title: string }; export type ResponseConfig = { title: string };
export type ResponseConditions = string; export type ResponseConditions = string;
export type ResponseDocumentTypes = DocumentType[];
export type ResponseDocuments = Document[];
export type HttpError = { export type HttpError = {
errors: string[]; errors: string[];

View File

@ -18,24 +18,3 @@
body { body {
background-color: var(--color-background); background-color: var(--color-background);
} }
/* Scroll bar stylings */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
/* Track */
::-webkit-scrollbar-track {
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 2px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.4);
}

View File

@ -4,14 +4,10 @@ import type { PageProps } from '@/types/page';
import { withError } from '@/utils/error'; import { withError } from '@/utils/error';
import { getPageUrlParams } from '@/utils/url'; import { getPageUrlParams } from '@/utils/url';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { headers } from 'next/headers';
export async function generateMetadata(pageProps: PageProps): Promise<Metadata> { export async function generateMetadata(pageProps: PageProps): Promise<Metadata> {
const pageUrlParams = getPageUrlParams(pageProps); const pageUrlParams = getPageUrlParams(pageProps);
const headersList = headers(); const { title } = await apiIUS.getConfig({ pageUrlParams });
const cookie = headersList.get('cookie') ?? '';
const { title } = await apiIUS.getConfig({ cookie, pageUrlParams });
const text = `Условия: ${title} | Эволюция`; const text = `Условия: ${title} | Эволюция`;
return { return {
@ -28,10 +24,7 @@ export default async function Page(pageProps: PageProps) {
return withError({ return withError({
render: async () => { render: async () => {
const pageUrlParams = getPageUrlParams(pageProps); const pageUrlParams = getPageUrlParams(pageProps);
const headersList = headers(); const conditions = await apiIUS.getConditions({ pageUrlParams });
const cookie = headersList.get('cookie') ?? '';
const conditions = await apiIUS.getConditions({ cookie, pageUrlParams });
return <Conditions html={conditions} />; return <Conditions html={conditions} />;
}, },

View File

@ -1,19 +1,13 @@
import * as apiIUS from '@/api/ius/query'; import * as apiIUS from '@/api/ius/query';
import { combineDocuments } from '@/api/ius/tools';
import { Form } from '@/components/Form'; import { Form } from '@/components/Form';
import type { FormComponentProps } from '@/components/Form/types';
import type { PageProps } from '@/types/page'; import type { PageProps } from '@/types/page';
import { withError } from '@/utils/error'; import { withError } from '@/utils/error';
import { getPageUrlParams } from '@/utils/url'; import { getPageUrlParams } from '@/utils/url';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { headers } from 'next/headers';
export async function generateMetadata(pageProps: PageProps): Promise<Metadata> { export async function generateMetadata(pageProps: PageProps): Promise<Metadata> {
const pageUrlParams = getPageUrlParams(pageProps); const pageUrlParams = getPageUrlParams(pageProps);
const headersList = headers(); const { title } = await apiIUS.getConfig({ pageUrlParams });
const cookie = headersList.get('cookie') ?? '';
const { title } = await apiIUS.getConfig({ cookie, pageUrlParams });
const text = `${title} | Эволюция`; const text = `${title} | Эволюция`;
return { return {
@ -31,24 +25,12 @@ export default async function Page(pageProps: PageProps) {
render: async () => { render: async () => {
const pageUrlParams = getPageUrlParams(pageProps); const pageUrlParams = getPageUrlParams(pageProps);
const headersList = headers();
const cookie = headersList.get('cookie') ?? '';
return Promise.all([ return Promise.all([
apiIUS.getData({ cookie, pageUrlParams }), apiIUS.getData({ pageUrlParams }),
apiIUS.getMetaData({ cookie, pageUrlParams }), apiIUS.getMetaData({ pageUrlParams }),
apiIUS.getConfig({ cookie, pageUrlParams }), apiIUS.getConfig({ pageUrlParams }),
apiIUS.getDocumentTypes({ cookie, pageUrlParams }), ]).then(([data, metaData, { title }]) => {
apiIUS.getDocuments({ cookie, pageUrlParams }), const props = { data, metaData, pageUrlParams, title };
]).then(([data, metaData, { title }, documentTypes, documents]) => {
const combinedDocuments = combineDocuments({ documentTypes, documents });
const props: FormComponentProps = {
combinedDocuments,
data,
metaData,
pageUrlParams,
title,
};
return <Form {...props} />; return <Form {...props} />;
}); });

View File

@ -1,9 +1,9 @@
import './globals.css'; import './globals.css';
import { Auth, Logo } from '@/components/layout'; import { Auth, Logo } from '@/components/layout';
import { Controls } from '@/components/layout/controls'; import { Controls } from '@/components/layout/controls';
import { Content, Header } from '@repo/ui';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import localFont from 'next/font/local'; import localFont from 'next/font/local';
import { Content, Header } from 'ui';
const inter = localFont({ const inter = localFont({
src: [ src: [
@ -46,17 +46,16 @@ export default function RootLayout({ children }: { readonly children: React.Reac
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.webmanifest" crossOrigin="use-credentials" />
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<Header> <Header>
<Logo /> <Logo />
<Auth /> <Auth />
</Header> </Header>
<Content> <div className="flex flex-col items-center ">
<Controls /> <Controls />
{children} <Content>{children}</Content>
</Content> </div>
</body> </body>
</html> </html>
); );

25
apps/web/app/manifest.ts Normal file
View File

@ -0,0 +1,25 @@
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
background_color: '#fff',
description: 'External | Эволюция',
display: 'standalone',
icons: [
{
sizes: '192x192',
src: '/android-chrome-192x192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: '/android-chrome-512x512.png',
type: 'image/png',
},
],
name: 'External | Эволюция',
short_name: 'External | Эволюция',
start_url: '/',
theme_color: '#fff',
};
}

View File

@ -1,4 +1,4 @@
import { HttpError } from '@repo/ui'; import { HttpError } from 'ui';
export default function NotFound() { export default function NotFound() {
return <HttpError code="404" title="Страница не найдена" />; return <HttpError code="404" title="Страница не найдена" />;

View File

@ -1,4 +1,4 @@
import { Background } from '@repo/ui'; import { Background } from 'ui';
type Props = { type Props = {
readonly html: string; readonly html: string;
@ -8,7 +8,7 @@ export function Conditions({ html }: Props) {
return ( return (
<Background <Background
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
className="grid w-full justify-center p-5 font-[calibri]" className="lg:w-standard grid w-full justify-center p-5 font-[calibri]"
/> />
); );
} }

View File

@ -0,0 +1,46 @@
import { FormContext } from './context/form-context';
import * as apiIus from '@/api/ius/query';
import { useFormStore } from '@/store/ius/form';
import { useContext } from 'react';
import { Button } from 'ui';
export function Buttons() {
const { reset, setValidation, values } = useFormStore();
const { pageUrlParams, setFormStatus } = useContext(FormContext);
return (
<div className="grid grid-cols-1 gap-2 gap-x-4 md:grid-cols-3">
<Button
intent="outline-danger"
onClick={() => {
reset();
}}
>
Отмена
</Button>
<Button
intent="secondary"
onClick={() => {
setFormStatus('pending');
}}
>
Возврат на доработку
</Button>
<Button
onClick={() => {
apiIus.validate({ pageUrlParams, payload: values }).then((res) => {
if (typeof res !== 'boolean') {
Object.keys(res.errors).forEach((name) => {
const elementValidation = res?.errors?.[name];
if (elementValidation)
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
});
}
});
}}
>
Сохранить
</Button>
</div>
);
}

View File

@ -1,95 +0,0 @@
/* eslint-disable react/jsx-curly-newline */
/* eslint-disable no-negated-condition */
import { FormContext } from '../context/form-context';
import * as apiIus from '@/api/ius/query';
import { useFormStore } from '@/store/ius/form';
import { Button } from '@repo/ui';
import { useCallback, useContext } from 'react';
const ERROR_RETRACT = 'Произошла ошибка при возврате на доработку';
const ERROR_SAVE = 'Произошла ошибка при сохранении';
export function Buttons() {
const { reset, resetValidation, setValidation, status, values } = useFormStore();
const { pageUrlParams, setFormState } = useContext(FormContext);
const handleSave = useCallback(() => {
setFormState({ status: 'pending' });
resetValidation();
apiIus.save({ pageUrlParams, payload: values }).then((res) => {
if (typeof res !== 'boolean') {
const { errors } = res;
if (Array.isArray(errors)) {
setFormState({ status: 'error', text: errors?.at(0) || ERROR_SAVE });
return;
}
Object.keys(errors).forEach((name) => {
const elementValidation = errors?.[name];
if (elementValidation)
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
});
setTimeout(() => {
setFormState({ status: 'edit' });
}, 300);
} else {
setFormState({ status: 'success' });
setTimeout(() => {
window.location.reload();
}, 500);
}
});
}, [pageUrlParams, resetValidation, setFormState, setValidation, values]);
const handleRetract = useCallback(() => {
setFormState({ status: 'pending' });
resetValidation();
apiIus.retract({ pageUrlParams, payload: values }).then((res) => {
if (typeof res !== 'boolean') {
const { errors } = res;
if (Array.isArray(errors)) {
setFormState({ status: 'error', text: errors?.at(0) || ERROR_RETRACT });
return;
}
setTimeout(() => {
setFormState({ status: 'edit' });
}, 300);
Object.keys(errors).forEach((name) => {
const elementValidation = errors?.[name];
if (elementValidation)
setValidation({ message: elementValidation[0] ?? '', name, valid: false });
});
} else {
setFormState({ status: 'success' });
setTimeout(() => {
window.location.reload();
}, 500);
}
});
}, [pageUrlParams, resetValidation, setFormState, setValidation, values]);
return (
<div className="grid grid-cols-1 gap-2 gap-x-4 md:grid-cols-3">
<Button
intent="outline-danger"
onClick={() => {
reset();
}}
>
Отмена
</Button>
<Button intent="outline-secondary" onClick={handleRetract}>
Возврат на доработку
</Button>
<Button onClick={handleSave} disabled={status !== 'edited'}>
Сохранить
</Button>
</div>
);
}

View File

@ -1,79 +0,0 @@
import type { FormComponentProps } from '../types';
import type { MetaObject } from '@/api/ius/types';
import { mapFieldTypeElement } from '@/config/elements';
import { useFormStore } from '@/store/ius/form';
import { ElementContainer } from '@repo/ui';
import { get } from 'radash';
import { useEffect } from 'react';
function RenderElement({
fieldType,
label,
max,
min = 0,
name,
visible,
...props
}: MetaObject & { readonly name: string }) {
const { setValue, validation, values } = useFormStore();
if (!visible) return false;
const Element = mapFieldTypeElement[fieldType];
return (
<ElementContainer
intent={validation[name] ? 'danger' : 'default'}
message={validation[name]?.message}
key={name}
id={name}
title={label}
>
<Element
loading={!Object.keys(values).length}
checked={fieldType === 'CHECKBOX' ? Boolean(values[name]) ?? false : false}
id={name}
value={values[name] ?? ''}
min={min}
max={max}
onChange={(e) => {
setValue({
name,
value:
fieldType === 'CHECKBOX' ? (e.target as HTMLInputElement).checked : e.target.value,
});
}}
{...props}
/>
</ElementContainer>
);
}
export function Elements({ data, metaData }: FormComponentProps) {
const { init } = useFormStore();
useEffect(() => {
init(data);
}, [data, init]);
const defaultElements = Object.keys(metaData).filter(
(x) => metaData[x]?.fieldType !== 'BIGSTRING'
);
const bigStringElements = Object.keys(metaData).filter(
(x) => metaData[x]?.fieldType === 'BIGSTRING'
);
return (
<>
<div className="mt-2 grid gap-2 gap-x-4 md:grid md:grid-cols-2 lg:grid-cols-3">
{defaultElements.map((name) => (
<RenderElement key={name} {...get(metaData, name)} name={name} />
))}
</div>
{bigStringElements.map((name) => (
<RenderElement key={name} {...get(metaData, name)} name={name} fieldType="BIGSTRING" />
))}
</>
);
}

View File

@ -1,2 +0,0 @@
export * from './Buttons';
export * from './Elements';

View File

@ -1,76 +0,0 @@
/* eslint-disable react/jsx-curly-newline */
/* eslint-disable no-negated-condition */
import { FormContext } from '../context/form-context';
import * as apiIus from '@/api/ius/query';
import type { HttpError } from '@/api/ius/types';
import { useFormStore } from '@/store/ius/form';
import { Button } from '@repo/ui';
import { pick } from 'radash';
import { useCallback, useContext } from 'react';
const ERROR_UPLOAD_DOCUMENT = 'Произошла ошибка при загрузке документов';
const SUCCESS_UPLOAD_DOCUMENTS = 'Файлы успешно загружены';
export function Buttons() {
const { resetValidation } = useFormStore();
const { formFiles, pageUrlParams, setFormFiles, setFormState } = useContext(FormContext);
const handleUploadFiles = useCallback(() => {
setFormState({ status: 'pending' });
resetValidation();
const uploadFiles = formFiles.map((formFile) => {
const formData = new FormData();
formData.append('file', formFile.file);
const document = pick(formFile, ['documentTypeId']);
return apiIus.uploadDocument({
document,
formData,
pageUrlParams,
});
});
return Promise.all(uploadFiles)
.then(async (res) => {
const errors = res.filter((x) => typeof x !== 'boolean') as HttpError[];
if (!errors.length) {
setFormState({
status: 'success',
text: SUCCESS_UPLOAD_DOCUMENTS,
});
setTimeout(() => {
window.location.reload();
}, 500);
return;
}
const error = errors.find((x) => x.errors.length)?.errors.at(0) || ERROR_UPLOAD_DOCUMENT;
setFormState({ status: 'error', text: error });
})
.catch((error) => {
setFormState({
status: 'error',
text: error ? JSON.stringify(error) : ERROR_UPLOAD_DOCUMENT,
});
});
}, [formFiles, pageUrlParams, resetValidation, setFormState]);
const handleCancel = () => {
setFormFiles([]);
};
return (
<div className="grid grid-cols-1 gap-2 gap-x-4 md:grid-cols-3">
<Button intent="outline-danger" onClick={handleCancel} disabled={!formFiles.length}>
Отмена
</Button>
<Button onClick={handleUploadFiles} disabled={!formFiles.length}>
Загрузить файлы
</Button>
</div>
);
}

View File

@ -1,75 +0,0 @@
import { FormContext } from '../context/form-context';
import type { FormComponentProps } from '../types';
import { ArrowDownTrayIcon } from '@heroicons/react/24/solid';
import { Heading, InputFile } from '@repo/ui';
import Link from 'next/link';
import { useContext, useEffect, useRef } from 'react';
type DownloadDocumentProps = Pick<FileProps, 'document'>;
function DownloadDocument({ document }: DownloadDocumentProps) {
return document?.href ? (
<Link
href={'/api/ius' + document.href}
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 h-10 px-5 py-2.5 text-sm font-medium hover:underline"
>
<div className="flex flex-row items-center gap-1">
<ArrowDownTrayIcon className="h-4 w-4" />
Скачать
</div>
</Link>
) : (
false
);
}
type FileProps = {
readonly document: FormComponentProps['combinedDocuments'][number];
};
function File({ document }: FileProps) {
const { formFiles, setFormFiles } = useContext(FormContext);
const { canUpload, documentTypeId, name } = document;
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files !== null) {
const file = event.target.files.item(0);
if (file)
setFormFiles([
...formFiles.filter((x) => x.documentTypeId !== documentTypeId),
{ documentTypeId, file, name },
]);
}
};
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const uploadableFile = formFiles.some((x) => x.documentTypeId === document.documentTypeId);
if (!uploadableFile && inputRef.current) inputRef.current.value = '';
}, [document.documentTypeId, formFiles]);
return (
<div key={documentTypeId} className="flex flex-col gap-1">
<label className="mb-2 block text-sm font-normal text-gray-900">{name}:</label>
<InputFile ref={inputRef} onChange={handleFileChange} disabled={!canUpload} />
<DownloadDocument document={document} />
</div>
);
}
export function Files({ combinedDocuments }: FormComponentProps) {
return (
<div className="grid gap-4">
<Heading className="text-sm">Документы</Heading>
<div className="grid gap-2 md:grid-cols-2">
{combinedDocuments.map((document) => (
<File key={document.documentTypeId} document={document} />
))}
</div>
</div>
);
}

View File

@ -1,2 +0,0 @@
export * from './Buttons';
export * from './Files';

View File

@ -0,0 +1,51 @@
import type { Props } from './types';
import { mapFieldTypeElement } from '@/config/elements';
import { useFormStore } from '@/store/ius/form';
import { useEffect } from 'react';
import { ElementContainer } from 'ui';
export function Elements({ data, metaData }: Props) {
const { init, setValue, validation, values } = useFormStore();
useEffect(() => {
init(data);
}, [data, init]);
return (
<div className="mt-2 grid auto-rows-auto grid-cols-1 gap-2 gap-x-4 md:grid-cols-2 lg:grid-cols-3">
{Object.keys(metaData).map((name) => {
const { fieldType, label, max, min = 0, visible, ...props } = metaData[name];
if (!visible) return false;
const Element = mapFieldTypeElement[fieldType];
return (
<ElementContainer
intent={validation[name] ? 'danger' : 'default'}
message={validation[name]?.message}
key={name}
id={name}
title={label}
>
<Element
loading={!Object.keys(values).length}
checked={fieldType === 'CHECKBOX' ? Boolean(values[name]) || false : false}
id={name}
value={values[name] || ''}
min={min}
max={max}
onChange={(e) => {
setValue({
name,
value: fieldType === 'CHECKBOX' ? e.target.checked : e.target.value,
});
}}
{...props}
/>
</ElementContainer>
);
})}
</div>
);
}

View File

@ -1,9 +1,9 @@
import { Heading } from '@repo/ui';
import Link from 'next/link'; import Link from 'next/link';
import { Heading } from 'ui';
export function Header({ link, title }: { readonly link: string; readonly title: string }) { export function Header({ link, title }: { readonly link: string; readonly title: string }) {
return ( return (
<div className="flex flex-col justify-between md:flex-row"> <div className="flex justify-between">
<Heading size={2}>{title}</Heading> <Heading size={2}>{title}</Heading>
<Link href={link} className="text-primary-600 text-sm font-medium hover:underline" prefetch> <Link href={link} className="text-primary-600 text-sm font-medium hover:underline" prefetch>
Посмотреть условия Посмотреть условия

View File

@ -1,61 +1,33 @@
'use client'; 'use client';
import { FormContext } from './context/form-context'; import { FormContext } from './context/form-context';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/solid'; import { CheckCircleIcon } from '@heroicons/react/24/solid';
import { Background, Button, LoadingSpinner } from '@repo/ui';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
import { Background, LoadingSpinner } from 'ui';
function OverlayWrapper({ children }: PropsWithChildren) { function OverlayWrapper({ children }: PropsWithChildren) {
return ( return (
<Background className="absolute left-0 top-0 grid h-full w-full place-items-center bg-opacity-80 backdrop-blur-sm"> <Background className="lg:w-standard absolute grid h-full w-full items-center justify-center border-none bg-opacity-80 backdrop-blur-sm">
<div className="absolute bottom-[50vh] flex flex-col items-center gap-2 md:relative md:bottom-0"> <div className="flex flex-row items-center gap-2">{children} </div>
{children}
</div>
</Background> </Background>
); );
} }
function StateContentWrapper({ children }: PropsWithChildren) {
return <div className="flex flex-row items-center gap-2">{children}</div>;
}
export function Overlay() { export function Overlay() {
const { formState, setFormState } = useContext(FormContext); const { formStatus } = useContext(FormContext);
const { status, text } = formState;
let stateContent: JSX.Element | false = false; if (formStatus === 'pending')
return (
if (status === 'pending') { <OverlayWrapper>
stateContent = (
<StateContentWrapper>
{LoadingSpinner} <p className="font-medium">Загрузка...</p> {LoadingSpinner} <p className="font-medium">Загрузка...</p>
</StateContentWrapper> </OverlayWrapper>
); );
} if (formStatus === 'success')
if (status === 'success') { return (
stateContent = ( <OverlayWrapper>
<StateContentWrapper> <CheckCircleIcon className="h-10 w-10 fill-green-500" title="OK" />{' '}
<CheckCircleIcon className="h-10 w-10 fill-green-500" title="OK" />
<p className="font-medium">Данные сохранены</p> <p className="font-medium">Данные сохранены</p>
</StateContentWrapper> </OverlayWrapper>
); );
}
if (status === 'error') { return false;
stateContent = (
<>
<StateContentWrapper>
<XCircleIcon className="h-10 w-10 fill-red-500" title="Error" />
<p className="font-medium">{text}</p>
</StateContentWrapper>{' '}
<Button type="button" intent="text" onClick={() => setFormState({ status: 'edit' })}>
Закрыть
</Button>
</>
);
}
if (!stateContent) return false;
return <OverlayWrapper>{stateContent}</OverlayWrapper>;
} }

View File

@ -1,19 +1,13 @@
import type { DocumentType } from '@/api/ius/types';
import type { PageUrlParams } from '@/utils/url'; import type { PageUrlParams } from '@/utils/url';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { createContext, useMemo, useState } from 'react'; import { createContext, useMemo, useState } from 'react';
type FormStatus = 'pending' | 'edit' | 'success' | 'error'; type FormStatus = 'pending' | 'edit' | 'success';
type FormState = { status: FormStatus; text?: string };
type FormFile = DocumentType & { file: File };
type ContextType = { type ContextType = {
readonly formFiles: FormFile[]; readonly formStatus: FormStatus;
readonly formState: FormState;
readonly pageUrlParams: PageUrlParams; readonly pageUrlParams: PageUrlParams;
readonly setFormFiles: (files: FormFile[]) => void; readonly setFormStatus: (status: FormStatus) => void;
readonly setFormState: (formState: FormState) => void;
}; };
export const FormContext = createContext<ContextType>({} as ContextType); export const FormContext = createContext<ContextType>({} as ContextType);
@ -22,11 +16,10 @@ export function FormContextProvider({
children, children,
...initialData ...initialData
}: PropsWithChildren & Pick<ContextType, 'pageUrlParams'>) { }: PropsWithChildren & Pick<ContextType, 'pageUrlParams'>) {
const [formState, setFormState] = useState<FormState>({ status: 'edit' }); const [formStatus, setFormStatus] = useState<FormStatus>('edit');
const [formFiles, setFormFiles] = useState<FormFile[]>([]);
const value = useMemo( const value = useMemo(
() => ({ ...initialData, formFiles, formState, setFormFiles, setFormState }), () => ({ ...initialData, formStatus, setFormStatus }),
[formFiles, formState, initialData] [formStatus, initialData]
); );
return <FormContext.Provider value={value}>{children}</FormContext.Provider>; return <FormContext.Provider value={value}>{children}</FormContext.Provider>;

View File

@ -1,34 +1,31 @@
'use client'; 'use client';
import * as Common from './Common'; import { Buttons } from './Buttons';
import { FormContext, FormContextProvider } from './context/form-context'; import { FormContext, FormContextProvider } from './context/form-context';
import * as Documents from './Documents'; import { Elements } from './Elements';
import { Header } from './Header'; import { Header } from './Header';
import { Overlay } from './Overlay'; import { Overlay } from './Overlay';
import type { FormComponentProps } from './types'; import type { Props } from './types';
import { createUrl } from '@/utils/url'; import { createUrl } from '@/utils/url';
import { Background, Divider } from '@repo/ui';
import type { FC } from 'react'; import type { FC } from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
import { Background, Divider } from 'ui';
function Content(props: FormComponentProps) { function Content(props: Props) {
const { title } = props; const { title } = props;
const { pageUrlParams } = useContext(FormContext); const { pageUrlParams } = useContext(FormContext);
return ( return (
<Background className="relative flex w-full flex-col gap-2 p-5"> <Background className="lg:w-standard relative grid w-full gap-2 p-5">
<Overlay /> <Overlay />
<Header title={title} link={'/ius' + createUrl({ ...pageUrlParams, route: '/conditions' })} /> <Header title={title} link={'/ius' + createUrl({ ...pageUrlParams, route: '/conditions' })} />
<Common.Elements {...props} /> <Elements {...props} />
<Common.Buttons />
<Divider />
<Documents.Files {...props} />
<Documents.Buttons />
<Divider /> <Divider />
<Buttons />
</Background> </Background>
); );
} }
function withContext<T extends FormComponentProps>(Component: FC<T>) { function withContext<T extends Props>(Component: FC<T>) {
return (props: T) => { return (props: T) => {
const { pageUrlParams } = props; const { pageUrlParams } = props;

View File

@ -1,11 +1,9 @@
import type { CombinedDocuments } from '@/api/ius/tools'; import type { ResponseGetData, ResponseMetaData } from '@/api/ius/types';
import type * as IUS from '@/api/ius/types';
import type { PageUrlParams } from '@/utils/url'; import type { PageUrlParams } from '@/utils/url';
export type FormComponentProps = { export type Props = {
readonly combinedDocuments: CombinedDocuments; readonly data: ResponseGetData;
readonly data: IUS.ResponseGetData; readonly metaData: ResponseMetaData;
readonly metaData: IUS.ResponseMetaData;
readonly pageUrlParams: PageUrlParams; readonly pageUrlParams: PageUrlParams;
readonly title: string; readonly title: string;
}; };

View File

@ -6,7 +6,7 @@ export function Controls() {
const router = useRouter(); const router = useRouter();
return ( return (
<div className="flex w-full flex-row px-5 pt-5"> <div className="lg:w-standard flex w-full flex-row px-5 pt-5">
<button <button
className="flex flex-row items-center gap-2 text-sm" className="flex flex-row items-center gap-2 text-sm"
type="button" type="button"

View File

@ -1,12 +1,10 @@
import { getClientEnv } from '@/config/env'; import { getEnv } from '@/config/env';
import Image from 'next/image'; import Image from 'next/image';
import logo from 'public/assets/images/logo-primary.svg'; import logo from 'public/assets/images/logo-primary.svg';
import logoDev from 'public/assets/images/logo-primary-dev.svg'; import logoDev from 'public/assets/images/logo-primary-dev.svg';
const env = getClientEnv(); const env = getEnv();
export function Logo() { export function Logo() {
return ( return <Image priority alt="logo" src={env.USE_DEV_COLORS ? logoDev : logo} height="24" />;
<Image priority alt="logo" src={env.NEXT_PUBLIC_USE_DEV_COLORS ? logoDev : logo} height="24" />
);
} }

View File

@ -1,12 +1,11 @@
import type { MetaObject } from '@/api/ius/types'; import type { MetaObject } from '@/api/ius/types';
import { Checkbox, Input, InputNumber, Textarea } from '@repo/ui'; import { Checkbox, Input, InputNumber } from 'ui';
function wrapMap<C, T extends Record<MetaObject['fieldType'], C>>(arg: T) { function wrapMap<C, T extends Record<MetaObject['fieldType'], C>>(arg: T) {
return arg; return arg;
} }
export const mapFieldTypeElement = wrapMap({ export const mapFieldTypeElement = wrapMap({
BIGSTRING: Textarea,
CHECKBOX: Checkbox, CHECKBOX: Checkbox,
DECIMAL: InputNumber, DECIMAL: InputNumber,
INT: InputNumber, INT: InputNumber,

View File

@ -1,6 +1,5 @@
const { clientEnvSchema, serverEnvSchema } = require('./schema/env'); const envSchema = require('./schema/env.js');
const getClientEnv = () => clientEnvSchema.parse(process.env); const getEnv = () => envSchema.parse(process.env);
const getServerEnv = () => serverEnvSchema.parse(process.env);
module.exports = { getClientEnv, getServerEnv }; module.exports = { getEnv };

View File

@ -2,15 +2,7 @@ const { z } = require('zod');
const envSchema = z.object({ const envSchema = z.object({
URL_IUS_DIRECT: z.string(), URL_IUS_DIRECT: z.string(),
NEXT_PUBLIC_USE_DEV_COLORS: z.string().optional(), USE_DEV_COLORS: z.string().optional(),
}); });
const serverEnvSchema = envSchema.pick({ module.exports = envSchema;
URL_IUS_DIRECT: true,
});
const clientEnvSchema = envSchema.pick({
NEXT_PUBLIC_USE_DEV_COLORS: true,
});
module.exports = { envSchema, serverEnvSchema, clientEnvSchema };

View File

@ -1,18 +1,9 @@
import { getServerEnv } from './env'; import { getEnv } from './env';
import proxyUrls from '@/constants/urls';
import { isServer } from '@/utils/common'; const { URL_IUS_DIRECT } = getEnv();
export function getUrls() { export function getUrls() {
if (isServer()) {
const env = getServerEnv();
const { URL_IUS_DIRECT } = env;
return {
URL_IUS: URL_IUS_DIRECT,
};
}
return { return {
URL_IUS: proxyUrls.URL_IUS_PROXY, URL_UIS: URL_IUS_DIRECT,
}; };
} }

View File

@ -1,4 +1,4 @@
import { getClientEnv } from '../config/env'; import { getEnv } from '../config/env';
export const COLORS_PROD = { export const COLORS_PROD = {
COLOR_DANGER: '#B20004', COLOR_DANGER: '#B20004',
@ -12,5 +12,5 @@ export const COLORS_DEV = {
COLOR_SECONDARY: '#c54a84', COLOR_SECONDARY: '#c54a84',
COLOR_TERTIARTY: '#FF9112', COLOR_TERTIARTY: '#FF9112',
}; };
const env = getClientEnv(); const env = getEnv();
export const COLORS = env.NEXT_PUBLIC_USE_DEV_COLORS ? COLORS_DEV : COLORS_PROD; export const COLORS = env.USE_DEV_COLORS ? COLORS_DEV : COLORS_PROD;

View File

@ -1,3 +0,0 @@
module.exports = {
URL_IUS_PROXY: '/api/ius',
};

View File

@ -1,12 +1,11 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { envSchema } = require('./config/schema/env'); const envSchema = require('./config/schema/env');
const urls = require('./constants/urls');
const env = envSchema.parse(process.env); const env = envSchema.parse(process.env);
const favicons = fs.readdirSync('./public/favicon/prod'); const favicons = fs.readdirSync('./public/favicon/prod');
const faviconSubPath = env.NEXT_PUBLIC_USE_DEV_COLORS ? '/favicon/dev' : '/favicon/prod'; const faviconSubPath = env.USE_DEV_COLORS ? '/favicon/dev' : '/favicon/prod';
function buildFaviconRewrite(source) { function buildFaviconRewrite(source) {
return { return {
destination: String.prototype.concat(faviconSubPath, source), destination: String.prototype.concat(faviconSubPath, source),
@ -24,17 +23,10 @@ const nextConfig = {
outputFileTracingRoot: path.join(__dirname, '../../'), outputFileTracingRoot: path.join(__dirname, '../../'),
}, },
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ['@repo/ui', 'radash'], transpilePackages: ['ui'],
async rewrites() { async rewrites() {
return [ return [...favicons.map((fileName) => buildFaviconRewrite(`/${fileName}`))];
{
destination: env.URL_IUS_DIRECT + '/:path*',
source: urls.URL_IUS_PROXY + '/:path*',
},
...favicons.map((fileName) => buildFaviconRewrite(`/${fileName}`)),
];
}, },
env,
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@ -10,21 +10,20 @@
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@repo/tsconfig": "workspace:*", "next": "^14.0.1",
"@repo/ui": "workspace:*",
"next": "^14.1.0",
"radash": "^11.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tsconfig": "*",
"ui": "*",
"wretch": "^2.7.0", "wretch": "^2.7.0",
"zod": "^3.22.4", "zod": "^3.22.4",
"zustand": "^4.5.0" "zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.6", "@tailwindcss/forms": "^0.5.6",
"@types/node": "^20.10.0", "@types/node": "^20.9.0",
"@types/react": "^18.2.39", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.15",
"@vchikalkin/eslint-config-awesome": "^1.1.5", "@vchikalkin/eslint-config-awesome": "^1.1.5",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.53.0", "eslint": "^8.53.0",

View File

@ -1,21 +0,0 @@
{
"background_color": "#fff",
"description": "External | Эволюция",
"display": "standalone",
"icons": [
{
"sizes": "192x192",
"src": "/android-chrome-192x192.png",
"type": "image/png"
},
{
"sizes": "512x512",
"src": "/android-chrome-512x512.png",
"type": "image/png"
}
],
"name": "External | Эволюция",
"short_name": "External | Эволюция",
"start_url": "/",
"theme_color": "#fff"
}

View File

@ -12,10 +12,8 @@ type FormState = {
defaultValues: Values; defaultValues: Values;
init: (values: Values) => void; init: (values: Values) => void;
reset: () => void; reset: () => void;
resetValidation: () => void;
setValidation: (input: { name: string } & ElementValidation) => void; setValidation: (input: { name: string } & ElementValidation) => void;
setValue: ({ name, value }: { name: string; value: Values[number] }) => void; setValue: ({ name, value }: { name: string; value: Values[number] }) => void;
status?: 'init' | 'edited';
validation: Record<string, ElementValidation | undefined>; validation: Record<string, ElementValidation | undefined>;
values: Values; values: Values;
}; };
@ -25,29 +23,24 @@ export const useFormStore = create<FormState>((set) => ({
init: (values) => init: (values) =>
set(() => ({ set(() => ({
defaultValues: values, defaultValues: values,
status: 'init',
values, values,
})), })),
reset: () => reset: () => {
set((state) => ({ set((state) => ({
status: 'init',
validation: {}, validation: {},
values: state.defaultValues, values: state.defaultValues,
})), }));
resetValidation: () => },
set(() => ({ setValidation: ({ message, name, valid }) => {
validation: {},
})),
setValidation: ({ message, name, valid }) =>
set((state) => ({ set((state) => ({
validation: { validation: {
...state.validation, ...state.validation,
[name]: { message, valid }, [name]: { message, valid },
}, },
})), }));
},
setValue: ({ name, value }) => setValue: ({ name, value }) =>
set((state) => ({ set((state) => ({
status: 'edited',
validation: { validation: {
...state.validation, ...state.validation,
[name]: undefined, [name]: undefined,

View File

@ -1,5 +1,5 @@
{ {
"extends": "@repo/tsconfig/nextjs.json", "extends": "tsconfig/nextjs.json",
"compilerOptions": { "compilerOptions": {
"plugins": [{ "name": "next" }], "plugins": [{ "name": "next" }],
"paths": { "paths": {

View File

@ -1,4 +1,4 @@
export type PageProps = { export type PageProps = {
params: { slug: string }; params: { slug: string };
searchParams: Record<string, string>; searchParams: string | string[][] | Record<string, string>;
}; };

View File

@ -1,5 +1,5 @@
import type * as t from '@/api/ius/types'; import type * as t from '@/api/ius/types';
import { HttpError } from '@repo/ui'; import { HttpError } from 'ui';
import type { WretchError } from 'wretch/types'; import type { WretchError } from 'wretch/types';
type Props = { type Props = {

View File

@ -1,7 +1,7 @@
import type { PageProps } from '@/types/page'; import type { PageProps } from '@/types/page';
export function getPageUrlParams({ params, searchParams }: PageProps) { export function getPageUrlParams({ params, searchParams }: PageProps) {
return { path: `/${params.slug}`, urlSearchParams: searchParams }; return { path: `/${params.slug}`, urlSearchParams: new URLSearchParams(searchParams) };
} }
export type PageUrlParams = ReturnType<typeof getPageUrlParams>; export type PageUrlParams = ReturnType<typeof getPageUrlParams>;

View File

@ -1,20 +0,0 @@
version: '3.3'
services:
external_client:
environment:
- URL_IUS_DIRECT=${URL_IUS_DIRECT}
- NEXT_PUBLIC_USE_DEV_COLORS=${NEXT_PUBLIC_USE_DEV_COLORS}
build:
args:
- URL_IUS_DIRECT=${URL_IUS_DIRECT}
- NEXT_PUBLIC_USE_DEV_COLORS=${NEXT_PUBLIC_USE_DEV_COLORS}
context: .
dockerfile: ./apps/web/Dockerfile
networks:
- external_network
restart: always
networks:
external_network:
external:
name: external_network

View File

@ -7,7 +7,7 @@
"lint:fix": "dotenv -e .env.local turbo run lint:fix", "lint:fix": "dotenv -e .env.local turbo run lint:fix",
"format": "prettier --write \"**/*.{ts,tsx,md}\"", "format": "prettier --write \"**/*.{ts,tsx,md}\"",
"prepare": "husky install", "prepare": "husky install",
"precommit": "pnpm format && pnpm lint:fix", "precommit": "yarn format && yarn lint:fix",
"clean": "turbo run clean" "clean": "turbo run clean"
}, },
"devDependencies": { "devDependencies": {
@ -17,14 +17,15 @@
"lint-staged": "^15.0.2", "lint-staged": "^15.0.2",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6", "prettier-plugin-tailwindcss": "^0.5.6",
"@repo/tsconfig": "workspace:*", "tsconfig": "*",
"turbo": "latest" "turbo": "latest"
}, },
"name": "Evo.External.App", "name": "Evo.External.App",
"packageManager": "pnpm@8.9.0", "packageManager": "yarn@1.22.19",
"engines": { "workspaces": [
"node": ">=18" "apps/*",
}, "packages/*"
],
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": [ "*.{js,jsx,ts,tsx}": [
"eslint --ignore-pattern **/graphql/* --ext js,jsx,ts,tsx --quiet --fix --" "eslint --ignore-pattern **/graphql/* --ext js,jsx,ts,tsx --quiet --fix --"

View File

@ -2,19 +2,20 @@
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"display": "Default", "display": "Default",
"compilerOptions": { "compilerOptions": {
"composite": false,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"incremental": false, "forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true, "isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"], "moduleResolution": "node",
"module": "NodeNext", "noUnusedLocals": false,
"moduleDetection": "force", "noUnusedParameters": false,
"moduleResolution": "NodeNext", "preserveWatchOutput": true,
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "ES2022" "strictNullChecks": true
} },
"exclude": ["node_modules"]
} }

View File

@ -4,10 +4,18 @@
"extends": "./base.json", "extends": "./base.json",
"compilerOptions": { "compilerOptions": {
"plugins": [{ "name": "next" }], "plugins": [{ "name": "next" }],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true, "allowJs": true,
"declaration": false,
"declarationMap": false,
"incremental": true,
"jsx": "preserve", "jsx": "preserve",
"noEmit": true "lib": ["dom", "dom.iterable", "esnext"],
} "module": "esnext",
"noEmit": true,
"resolveJsonModule": true,
"strict": false,
"target": "es5"
},
"include": ["src", "next-env.d.ts"],
"exclude": ["node_modules"]
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "@repo/tsconfig", "name": "tsconfig",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"license": "MIT", "license": "MIT",

View File

@ -3,6 +3,9 @@
"display": "React Library", "display": "React Library",
"extends": "./base.json", "extends": "./base.json",
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx" "jsx": "react-jsx",
"lib": ["ES2015", "DOM"],
"module": "ESNext",
"target": "es6"
} }
} }

View File

@ -5,7 +5,7 @@ import type { ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
const variants = cva( const variants = cva(
'rounded-sm h-10 px-5 py-2.5 text-sm font-semibold text-white transition-all ease-in-out', 'rounded-sm h-10 px-5 py-2.5 text-sm font-medium text-white transition-all ease-in-out',
{ {
defaultVariants: { defaultVariants: {
intent: 'default', intent: 'default',
@ -13,16 +13,14 @@ const variants = cva(
variants: { variants: {
intent: { intent: {
danger: 'bg-danger hover:bg-danger-700', danger: 'bg-danger hover:bg-danger-700',
default: 'bg-primary hover:bg-primary-600', default: 'bg-primary hover:bg-primary-700',
'outline-danger': 'outline-danger':
'bg-transparent text-danger border border-danger hover:bg-danger hover:text-white', 'bg-transparent text-danger border border-danger hover:bg-danger hover:text-white',
'outline-default': 'outline-default':
'bg-transparent text-primary border border-primary hover:bg-primary hover:text-white', 'bg-transparent text-primary border border-primary hover:bg-primary hover:text-white',
'outline-secondary': 'outline-secondary':
'border border-primary text-primary-500 hover:bg-primary-500 hover:text-white', 'opacity-70 bg-transparent text-secondary border border-secondary hover:bg-secondary hover:text-white',
secondary: secondary: 'bg-secondary hover:bg-secondary-700',
'bg-primary-50 text-primary-500 border border-transparent hover:bg-primary-100 hover:text-primary-600',
text: 'bg-none text-primary-500 hover:bg-primary-50',
}, },
}, },
} }

View File

@ -5,7 +5,7 @@ import { forwardRef, type HTMLAttributes, type PropsWithChildren } from 'react';
export type ContainerProps = HTMLAttributes<HTMLDivElement> & PropsWithChildren; export type ContainerProps = HTMLAttributes<HTMLDivElement> & PropsWithChildren;
const variants = cva('flex min-h-[36px] items-center', { const variants = cva('flex h-9 items-center', {
defaultVariants: { defaultVariants: {
intent: 'default', intent: 'default',
}, },

View File

@ -1,9 +1,5 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
export function Content({ children }: PropsWithChildren) { export function Content({ children }: PropsWithChildren) {
return ( return <div className="flex w-full justify-center gap-2 py-5">{children}</div>;
<div className="grid w-full place-items-center">
<div className="xl:w-standard grid w-screen gap-5">{children}</div>
</div>
);
} }

View File

@ -6,7 +6,7 @@ import { forwardRef } from 'react';
const variants = cva('font-bold leading-7 text-gray-900 sm:truncate sm:tracking-tight', { const variants = cva('font-bold leading-7 text-gray-900 sm:truncate sm:tracking-tight', {
defaultVariants: { defaultVariants: {
size: 1, size: 'default',
}, },
variants: { variants: {
size: { size: {
@ -19,6 +19,7 @@ const variants = cva('font-bold leading-7 text-gray-900 sm:truncate sm:tracking-
7: 'text-6xl sm:text-7xl', 7: 'text-6xl sm:text-7xl',
8: 'text-7xl sm:text-8xl', 8: 'text-7xl sm:text-8xl',
9: 'text-8xl sm:text-9xl', 9: 'text-8xl sm:text-9xl',
default: 'text-lg',
}, },
}, },
}); });

View File

@ -12,4 +12,3 @@ export * from './http-error';
export * from './icons'; export * from './icons';
export * from './input'; export * from './input';
export * from './select'; export * from './select';
export * from './textarea';

View File

@ -21,15 +21,3 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
export const InputNumber = forwardRef<HTMLInputElement, InputProps>((props, ref) => ( export const InputNumber = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<Input ref={ref} type="number" {...props} /> <Input ref={ref} type="number" {...props} />
)); ));
export const InputFile = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input
{...props}
ref={ref}
type="file"
className="file:bg-primary-50 file:text-primary-500 hover:file:bg-primary-100 block w-full text-sm
text-slate-500 file:mr-4 file:rounded-sm file:border-0
file:px-4 file:py-2 file:text-sm file:font-semibold
hover:file:cursor-pointer disabled:cursor-not-allowed disabled:opacity-30 disabled:file:cursor-not-allowed"
/>
));

View File

@ -1,6 +1,8 @@
{ {
"name": "@repo/ui", "name": "ui",
"version": "0.0.0", "version": "0.0.0",
"main": "./index.tsx",
"types": "./index.tsx",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
@ -8,15 +10,15 @@
}, },
"devDependencies": { "devDependencies": {
"@turbo/gen": "^1.10.16", "@turbo/gen": "^1.10.16",
"@types/node": "^20.10.0", "@types/node": "^20.8.10",
"@types/react": "^18.2.39", "@types/react": "^18.2.34",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.14",
"@vchikalkin/eslint-config-awesome": "^1.1.5", "@vchikalkin/eslint-config-awesome": "^1.1.5",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"@repo/tsconfig": "workspace:*", "tsconfig": "*",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
} }

View File

@ -1,19 +0,0 @@
import { cn } from './utils';
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
import { forwardRef } from 'react';
const variants = cva(
'focus:ring-0 min-h-[36px] resize-y h-auto hover:border-primary-500 focus:border-primary-500 w-full rounded-sm border disabled:hover:border-gray-300 border-gray-300 p-2 px-3 text-sm text-gray-900 outline-none transition-colors ease-in-out disabled:cursor-not-allowed disabled:text-opacity-30'
);
export type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &
VariantProps<typeof variants> & { readonly loading?: boolean };
export const Textarea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ className, loading, ...props }, ref) => {
if (loading) return <div className="h-[98px] w-full animate-pulse rounded bg-gray-100" />;
return <textarea rows={4} ref={ref} className={cn(variants({ className }))} {...props} />;
}
);

View File

@ -1,5 +1,5 @@
{ {
"extends": "@repo/tsconfig/react-library.json", "extends": "tsconfig/react-library.json",
"include": ["."], "include": ["."],
"exclude": ["dist", "build", "node_modules"] "exclude": ["dist", "build", "node_modules"]
} }

7671
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
packages:
- "apps/*"
- "packages/*"

View File

@ -1,3 +1,3 @@
{ {
"extends": "@repo/tsconfig/base.json" "extends": "tsconfig/base.json"
} }

5986
yarn.lock Normal file

File diff suppressed because it is too large Load Diff