Replace UI module with Next.js

This commit is contained in:
Roman Jaroš 2023-12-28 21:58:08 +00:00
parent 8aeff18162
commit 616205fe73
91 changed files with 3570 additions and 722 deletions

View file

@ -3,16 +3,13 @@
"workspaceFolder": "/home/project/seedling",
"dockerComposeFile": "docker-compose.yml",
"service": "seedling",
"postCreateCommand": "sudo chown iamuser:iamuser -R /home/project",
"postCreateCommand": "sudo chown iamuser:iamuser /home/project",
"runServices": [
"selenium"
],
"customizations": {
"vscode": {
"extensions": [
"Fooxly.themeswitch",
"EditorConfig.EditorConfig",
"rvest.vs-code-prettier-eslint",
"wix.vscode-import-cost",
"mia-hall.vscode-git-branch-sidebar"
]
"extensions": []
}
}
}

View file

@ -1,18 +1,42 @@
version: "3"
name: seedling
volumes:
node_modules:
pnpm-store:
networks:
net:
name: seedling_net
driver: bridge
ipam:
config:
- subnet: 10.2.1.0/24
gateway: 10.2.1.1
services:
seedling:
build:
context: .
container_name: seedling
volumes:
- ../:/home/project/seedling
- ../:/home/project/seedling:delegated
- node_modules:/home/project/seedling/node_modules
- pnpm-store:/home/project/seedling/.pnpm-store
command: sleep infinity
command: sleep infinity
networks:
net:
ipv4_address: 10.2.1.2
selenium:
image: seleniarm/standalone-chromium:114.0
container_name: seedling-selenium
shm_size: 2gb
environment:
- SE_VNC_NO_PASSWORD=1
ports:
- 7900:7900
- 5900:5900
- 4444:4444
networks:
net:
ipv4_address: 10.2.1.4

7
.gitignore vendored
View file

@ -1,10 +1,9 @@
build/
dist/
app/
node_modules/
.vscode/
.idea/
.pnpm/
.pnpm-store/
node_modules/
.pnpm-store/
output/

View file

@ -15,6 +15,7 @@ const contextDir = `${argv._[0] ?? defaultContextDir}`;
const appsDir = `${contextDir}/apps`;
const uiDir = `${contextDir}/apps/${appName}-ui`;
const restDir = `${contextDir}/apps/${appName}-rest`;
const testDir = `${contextDir}/apps/${appName}-selenium`;
// prepare structure folders
if (!fs.existsSync(uiDir)) {
@ -26,11 +27,13 @@ if (!fs.existsSync(uiDir)) {
fs.mkdirSync(`${uiDir}/src`);
fs.mkdirSync(restDir);
fs.mkdirSync(`${restDir}/src`);
fs.mkdirSync(testDir);
fs.mkdirSync(`${testDir}/src`);
}
// copy folder content
try {
fs.cpSync(`${rootDir}/source/common/`, contextDir, {
fs.cpSync(`${rootDir}/source/__/`, contextDir, {
force: true,
recursive: true,
});
@ -42,6 +45,10 @@ try {
force: true,
recursive: true,
});
fs.cpSync(`${rootDir}/source/selenium/`, testDir, {
force: true,
recursive: true,
});
} catch (err) {
console.error(err);
}
@ -56,6 +63,8 @@ fs.renameSync(`${contextDir}/prettierrc`, `${contextDir}/.prettierrc`);
fs.renameSync(`${contextDir}/.tsconfig.json`, `${contextDir}/tsconfig.json`);
fs.renameSync(`${uiDir}/.tsconfig.json`, `${uiDir}/tsconfig.json`);
fs.renameSync(`${restDir}/.tsconfig.json`, `${restDir}/tsconfig.json`);
fs.renameSync(`${testDir}/.tsconfig.json`, `${testDir}/tsconfig.json`);
fs.renameSync(`${testDir}/src/.tsconfig.json`, `${testDir}/src/tsconfig.json`);
// replace in files
replaceInFiles.sync({

View file

@ -7,8 +7,8 @@
"seedling": "./create.js"
},
"scripts": {
"create": "pnpm ci:build",
"ci:build": "node create.js --name app --port 90 ./app",
"ci:build": "pnpm build",
"build": "node create.js --name app --port 90 ./output",
"release": "pnpm version --no-git-tag-version"
},
"dependencies": {

View file

@ -0,0 +1,42 @@
version: "3"
volumes:
node_modules:
pnpm-store:
networks:
net:
name: $(appName)_net
driver: bridge
ipam:
config:
- subnet: 10.2.0.0/24
gateway: 10.2.0.1
services:
seedling:
build:
context: .
container_name: $(appName)
volumes:
- ../:/home/project/$(appName)
- node_modules:/home/project/seedling/node_modules
- pnpm-store:/home/project/seedling/.pnpm-store
command: sleep infinity
networks:
net:
ipv4_address: 10.2.0.2
selenium:
image: seleniarm/standalone-chromium:114.0
container_name: $(appName)-selenium
shm_size: 2gb
environment:
- SE_VNC_NO_PASSWORD=1
ports:
- 7900:7900
- 5900:5900
- 4444:4444
networks:
net:
ipv4_address: 10.2.0.4

14
source/__/.tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"module": "commonjs",
"target": "es6"
},
"exclude": [
"./node_modules"
]
}

View file

@ -1,7 +1,8 @@
build/
dist/
node_modules/
.vscode/
.idea/
node_modules/
.pnpm-store/

View file

@ -4,9 +4,10 @@
"author": "Roman Jaroš",
"license": "ISC",
"scripts": {
"dev": "pnpm -r dev",
"dev": "pnpm -r --parallel --stream dev",
"test": "pnpm -r test",
"test:e2e": "pnpm -r test:e2e"
"test:e2e": "pnpm -r test:e2e",
"selenium:smoke": "pnpm -r selenium:smoke"
},
"dependencies": {},
"devDependencies": {

View file

@ -1,18 +0,0 @@
version: "3"
name: $(appName)
volumes:
node_modules:
pnpm-store:
services:
seedling:
build:
context: .
container_name: $(appName)
volumes:
- ../:/home/project/$(appName)
- node_modules:/home/project/seedling/node_modules
- pnpm-store:/home/project/seedling/.pnpm-store
command: sleep infinity

View file

@ -1,24 +0,0 @@
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"sourceMap": true,
"esModuleInterop": true,
"allowJs": true,
"outDir": "build",
"module": "commonjs",
"target": "es6",
"lib": [
"es6",
"dom"
],
},
"exclude": [
"./node_modules",
"./src/**/__tests__/*.tsx"
]
}

View file

@ -1,9 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"noImplicitAny": true,
"strictFunctionTypes": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowJs": true,
"outDir": "./dist",
"baseUrl": "./"
"lib": [
"es6",
"dom"
],
"baseUrl": "."
}
}

View file

@ -1,15 +1,15 @@
/** @type {import('jest').Config} */
const config = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
testRegex: '.\\.test\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};
module.exports = config;

View file

@ -7,7 +7,7 @@
"scripts": {
"nest": "nest",
"build": "nest build",
"dev": "nest start --watch",
"dev": "nest start --watch --preserveWatchOutput",
"dev:debug": "nest start --debug --watch",
"start": "node dist/main",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@ -16,7 +16,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest.config.js"
},
"dependencies": {
"@nestjs/axios": "3.0.1",

View file

@ -4,7 +4,7 @@ const config = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testEnvironment: 'node',
testRegex: '.e2e-spec.ts$',
testRegex: '.spec.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},

83
source/selenium/.eslintrc Normal file
View file

@ -0,0 +1,83 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": [
"typescript",
"@typescript-eslint",
"typescript-sort-keys",
"simple-import-sort",
"import"
],
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
}
},
"overrides": [
{
"files": [
"*.js"
],
"parser": "esprima",
"rules": {
"@typescript-eslint/no-var-requires": 0
}
}
],
"ignorePatterns": [],
"rules": {
"max-len": "off",
"no-useless-escape": "off",
"object-curly-spacing": [
"error",
"always"
],
"no-multi-spaces": "error",
"no-console": [
"error",
{
"allow": [
"info",
"warn",
"error"
]
}
],
"no-unused-vars": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": 2,
"@typescript-eslint/interface-name-prefix": 0,
"@typescript-eslint/no-empty-interface:": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-member-accessibility": 2,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-empty-function": 0,
"typescript-sort-keys/interface": "error",
"typescript-sort-keys/string-enum": "error",
"sort-imports": "off",
"import/no-duplicates": "error",
"import/order": "off",
"simple-import-sort/imports": [
"error",
{
"groups": [
[
"^\\u0000"
],
[
"^[^.]"
],
[
"^\\."
]
]
}
]
}
}

View file

@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"types": [
"./global.d.ts"
]
},
"include": [
"**/*.ts"
]
}

View file

@ -0,0 +1,6 @@
# Cucumber
SELENIUM_SERVER_URL="http://10.0.1.22:4444"
# Application
# HOST_IP="" - defined in Jenkins node
APP_PORT="9092"

View file

@ -0,0 +1,6 @@
# Cucumber
SELENIUM_SERVER_URL="http://10.2.1.4:4444"
# Application
HOST_IP="http://10.2.1.2"
APP_PORT="3000"

View file

@ -0,0 +1,24 @@
const argv = require('minimist')(process.argv);
const common = [
'src/features/home.feature',
'src/features/*.feature',
'--require-module ts-node/register',
'--require src/steps/**/*.ts',
'-f summary',
'-f json:report/report.json',
'-f @qavajs/html-formatter:report/report.html',
].join(' ');
module.exports = (async function () {
if (argv['p'] === 'ci') {
require('dotenv').config({ path: './config/.ci.env' });
} else {
require('dotenv').config({ path: './config/.local.env' });
}
return {
default: common,
ci: common,
};
})();

11
source/selenium/global.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
APP_PORT: string;
HOST_IP: string;
SELENIUM_SERVER_URL: string;
}
}
}
export default global;

View file

@ -0,0 +1,38 @@
{
"name": "$(appName)-selenium",
"version": "0.1.0",
"author": "Roman Jaroš",
"license": "ISC",
"scripts": {
"selenium:smoke": "cucumber-js"
},
"dependencies": {
"typescript": "5.3.3"
},
"devDependencies": {
"@qavajs/html-formatter": "0.15.3",
"@types/chai": "4.3.11",
"@types/selenium-webdriver": "4.1.21",
"@typescript-eslint/eslint-plugin": "6.15.0",
"@typescript-eslint/parser": "6.15.0",
"@cucumber/cucumber": "10.0.1",
"dotenv": "16.3.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-config-next": "14.0.4",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-typescript": "0.14.0",
"eslint-plugin-typescript-sort-keys": "3.1.0",
"chai": "4.3.10",
"cucumber-tsflow": "4.3.1",
"minimist": "1.2.8",
"prettier": "3.1.1",
"selenium-webdriver": "4.16.0",
"ts-loader": "9.5.1",
"ts-node": "10.9.2"
},
"peerDependencies": {}
}

View file

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "node",
"emitDecoratorMetadata": false,
"typeRoots": [
"../"
]
},
"ts-node": {
"transpileOnly": true
}
}

View file

@ -0,0 +1,17 @@
import { By } from 'selenium-webdriver';
export class BasePage {
protected url: string;
public title?: string;
public root: By;
public buttons?: Record<string, By>;
public fields?: Record<string, By>;
public forms?: Record<string, By>;
public messages?: Record<string, By>;
public getPageUrl() {
return `${process.env.HOST_IP}:${process.env.APP_PORT}` + this.url;
}
}

View file

@ -0,0 +1,13 @@
import { setDefaultTimeout } from '@cucumber/cucumber';
import { ThenableWebDriver } from 'selenium-webdriver';
import { WebDriver } from './WebDriver';
export class BaseStep {
protected driver: ThenableWebDriver;
public constructor() {
this.driver = new WebDriver().getDriver();
setDefaultTimeout(10 * 60 * 1000);
}
}

View file

@ -0,0 +1,25 @@
import { binding } from 'cucumber-tsflow';
import { Builder, Capabilities, ThenableWebDriver } from 'selenium-webdriver';
const capabilities = Capabilities.chrome();
capabilities.set('chromeOptions', { w3c: false });
@binding()
export class WebDriver {
protected readonly driver: ThenableWebDriver;
private static instance: WebDriver;
public constructor() {
if (WebDriver.instance) {
return WebDriver.instance;
}
this.driver = new Builder().usingServer(process.env.SELENIUM_SERVER_URL).withCapabilities(capabilities).build();
this.driver.manage().setTimeouts({ implicit: 100 });
WebDriver.instance = this;
}
public getDriver() {
return this.driver;
}
}

View file

@ -0,0 +1,5 @@
Feature: Home page
Scenario: Verify home page title
Given i visit home page
Then page title is $(AppName)

View file

@ -0,0 +1,15 @@
import { binding } from 'cucumber-tsflow';
import { By } from 'selenium-webdriver';
import { BasePage } from '../common/BasePage';
@binding()
export class HomePage extends BasePage {
protected url = '/';
// initial div
public root = By.xpath('//body');
// page buttons
public buttons = {};
}

View file

@ -0,0 +1,6 @@
import { By } from 'selenium-webdriver';
export class Navigation {
public root = By.xpath('//div[@class="page-top"]');
public logoutLink = By.xpath(`${this.root.value}//div[contains(text(), "Odhlásit se")]`);
}

View file

@ -0,0 +1,20 @@
import { expect } from 'chai';
import { binding, given } from 'cucumber-tsflow';
import { ThenableWebDriver } from 'selenium-webdriver';
import { WebDriver } from '../../common/WebDriver';
@binding([])
export class PageSteps {
protected driver: ThenableWebDriver;
public constructor() {
this.driver = new WebDriver().getDriver();
}
@given(/page title is (.*)/)
public async thenPageTitleIs(title: string) {
const pageTitle = await this.driver.getTitle();
expect(pageTitle).to.equal(title);
}
}

View file

@ -0,0 +1,24 @@
import { afterAll, binding } from 'cucumber-tsflow';
import { until } from 'selenium-webdriver';
import { WebDriver } from '../../common/WebDriver';
import { Navigation } from '../../pages/components/Navigation';
@binding([])
export class SessionSteps {
@afterAll()
protected async closeBrowser() {
const driver = new WebDriver().getDriver();
await driver.quit();
}
@afterAll()
protected async logout() {
const driver = new WebDriver().getDriver();
const webElements = await driver.findElements(new Navigation().logoutLink);
if (webElements.length > 0) {
await webElements[0]?.click?.();
await driver.wait(until.urlContains(`${process.env.HOST_IP}:${process.env.APP_PORT}`));
}
}
}

View file

@ -0,0 +1,16 @@
import { binding, given } from 'cucumber-tsflow';
import { BaseStep } from '../common/BaseStep';
import { HomePage } from '../pages/HomePage';
@binding([HomePage])
export class HomePageSteps extends BaseStep {
public constructor(public readonly homePage: HomePage) {
super();
}
@given(/i visit home page/)
public async givenIamOnHomePage() {
await this.driver.get(this.homePage.getPageUrl());
}
}

View file

@ -1,10 +1,10 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
"prettier",
"next/core-web-vitals"
],
"plugins": [
"react",

View file

@ -1,54 +1,34 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"api/*": [
"./src/api/*"
],
"app/*": [
"./src/app/*"
],
"components/*": [
"./src/components/*"
],
"constants/*": [
"./src/constants/*"
],
"features/*": [
"./src/features/*"
],
"hooks/*": [
"./src/hooks/*"
],
"localization/*": [
"./src/localization/*"
],
"pages/*": [
"./src/pages/*"
],
"utils/*": [
"./src/utils/*"
],
"types/*": [
"./src/types/*"
]
},
"types": [
"node",
"jest",
"./src/types/yup"
]
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
"./types/modules.d.ts",
"src/styles/theme.js"
],
"exclude": [
"./node_modules",
"./src/**/__tests__/*.tsx"
]
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": [
"node",
"jest"
],
"strict": false,
"noEmit": true,
"plugins": [
{
"name": "next"
}
],
"lib": [
"dom",
"dom.iterable",
"esnext"
]
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"./src/**/__tests__/*.tsx"
]
}

View file

@ -1 +1,36 @@
# $(AppName)
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
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View file

@ -1 +0,0 @@
ENDPOINT_BASE_URL=""

View file

@ -1 +0,0 @@
ENDPOINT_BASE_URL=""

View file

@ -1,12 +1,27 @@
FROM nginx:1.25.3-alpine
FROM node:18-alpine3.19
RUN mkdir app
RUN mkdir -p /run/nginx
RUN apk add --no-cache libc6-compat
COPY build /app
COPY docker/nginx/conf.d/ /etc/nginx/conf.d/
WORKDIR /app
EXPOSE 80
ENV NODE_ENV production
ENV PORT 3000
VOLUME [ "/etc/nginx/conf.d" ]
ENTRYPOINT ["nginx", "-g", "daemon off;"]
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY .next/ .next/
COPY public/ public/
COPY node_modules/ node_modules/
COPY package.json package.json
COPY next.config.js next.config.js
RUN chown -R nextjs:nodejs .next
USER nextjs
EXPOSE 3000
CMD ["npm", "run", "start"]

View file

@ -1,13 +0,0 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View file

@ -1,21 +0,0 @@
module.exports = {
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(-|/)(test))\\.(ts|tsx)?$',
transformIgnorePatterns: ['<rootDir>/node_modules/.*'],
testPathIgnorePatterns: ['.vscode'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
moduleNameMapper: {
'^app(.*)$': '<rootDir>/src/app/$1',
'^api(.*)$': '<rootDir>/src/api/$1',
'^components(.*)$': '<rootDir>/src/components/$1',
'^constants(.*)$': '<rootDir>/src/constants/$1',
'^features(.*)$': '<rootDir>/src/features/$1',
'^hooks(.*)$': '<rootDir>/src/hooks/$1',
'^localization(.*)$': '<rootDir>/src/localization/$1',
'^pages(.*)$': '<rootDir>/src/pages/$1',
'^types(.*)$': '<rootDir>/src/types/$1',
'^utils(.*)$': '<rootDir>/src/utils/$1',
},
};

4
source/ui/next.config.js Normal file
View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;

View file

@ -1,41 +0,0 @@
/** @type {import('nightwatch').NightwatchOptions} */
module.exports = {
src_folders: 'tests/.nightwatchjs/src',
output_folder: 'tests/.nightwatchjs/output',
page_objects_path: ['tests/.nightwatchjs/objects'],
custom_commands_path: 'tests/.nightwatchjs/commands',
use_xpath: true,
end_session_on_fail: true,
start_process: false,
test_settings: {
localhost: {
launch_url: '',
selenium_port: 4444,
selenium_host: '10.2.0.4',
screenshots: {
enabled: true,
on_failure: true,
on_error: true,
path: 'tests/.nightwatchjs/output',
},
desiredCapabilities: {
browserName: 'chrome',
chromeOptions: {
args: ['no-sandbox'],
},
},
},
ci: {
launch_url: '',
selenium_port: 4444,
selenium_host: '10.0.1.21',
desiredCapabilities: {
browserName: 'chrome',
chromeOptions: {
args: ['headless', 'no-sandbox'],
},
},
},
},
};

View file

@ -1,95 +1,73 @@
{
"name": "$(appName)-ui",
"version": "0.1.0",
"author": "Roman Jaroš",
"license": "ISC",
"scripts": {
"ci:build": "pnpm build:prod",
"ci:build:test": "pnpm build:test",
"ci:test:e2e": "nightwatch --env ci",
"dev": "webpack serve --config scripts/webpack-dev.js --env config=local",
"lint": "eslint -c .eslintrc src/**/*.{ts,tsx}",
"lint:css": "stylelint src/**/*.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:update": "jest -u",
"test:e2e-build": "tsc -p tests/tsconfig.json -w",
"test:e2e": "nightwatch --env localhost",
"build:prod": "webpack --config scripts/webpack-prod.js",
"build:test": "webpack --config scripts/webpack-dev.js --env config=test",
"serve": "http-server-spa build index.html 3331",
"codegen": "pnpm dlx @rtk-query/codegen-openapi openapi-config.ts"
},
"dependencies": {
"@procyon/api": "1.2.9",
"@procyon/auth": "1.2.9",
"@procyon/components": "1.2.9",
"@procyon/constants": "1.2.9",
"@procyon/forms": "1.2.9",
"@procyon/localization": "1.2.9",
"@procyon/styles": "1.2.9",
"@procyon/types": "1.2.9",
"@procyon/utils": "1.2.9",
"@reduxjs/toolkit": "2.0.1",
"clsx": "2.0.0",
"date-fns": "3.0.6",
"history": "5.3.0",
"ramda": "0.29.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-redux": "9.0.4",
"typescript": "5.3.3",
"wouter": "2.12.1",
"yup": "1.3.3"
},
"devDependencies": {
"@rtk-query/codegen-openapi": "1.2.0",
"@types/enzyme": "3.10.16",
"@types/jest": "29.5.11",
"@types/nightwatch": "2.3.30",
"@types/ramda": "0.29.9",
"@types/react": "18.2.45",
"@types/react-dom": "18.2.18",
"@types/react-redux": "7.1.33",
"@types/yup": "0.29.14",
"@typescript-eslint/eslint-plugin": "6.15.0",
"@typescript-eslint/parser": "6.15.0",
"autoprefixer": "10.4.16",
"case-sensitive-paths-webpack-plugin": "2.4.0",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.8.1",
"process": "0.11.10",
"dotenv": "16.3.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-typescript": "0.14.0",
"eslint-plugin-typescript-sort-keys": "3.1.0",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.0",
"http-server-spa": "1.3.0",
"jest": "29.7.0",
"mini-css-extract-plugin": "2.7.6",
"nightwatch": "3.3.5",
"postcss": "8.4.32",
"postcss-loader": "7.3.3",
"prettier": "3.1.1",
"tailwindcss": "3.4.0",
"ts-node": "10.9.2",
"ts-jest": "29.1.1",
"ts-loader": "9.5.1",
"ts-prune": "0.10.3",
"tsconfig-paths": "4.2.0",
"url-loader": "4.1.1",
"webpack": "5.89.0",
"webpack-bundle-analyzer": "4.10.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.10.0"
},
"peerDependencies": {}
"name": "$(appName)-ui",
"version": "0.1.0",
"author": "Roman Jaroš",
"license": "ISC",
"scripts": {
"ci:build": "pnpm build:prod",
"ci:in-test:smoke": "pnpm in-test:smoke -p ci",
"dev": "next dev",
"start": "next start",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:css": "stylelint src/**/*.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:update": "jest -u",
"build:prod": "next build",
"codegen": "pnpm dlx @rtk-query/codegen-openapi openapi-config.ts"
},
"dependencies": {
"@procyon/api": "1.2.10",
"@procyon/auth": "1.2.10",
"@procyon/components": "1.2.10",
"@procyon/constants": "1.2.10",
"@procyon/forms": "1.2.10",
"@procyon/localization": "1.2.10",
"@procyon/styles": "1.2.10",
"@procyon/types": "1.2.10",
"@procyon/utils": "1.2.10",
"@reduxjs/toolkit": "2.0.1",
"clsx": "2.0.0",
"date-fns": "3.0.6",
"history": "5.3.0",
"next": "14.0.4",
"ramda": "0.29.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-redux": "9.0.4",
"typescript": "5.3.3",
"yup": "1.3.3"
},
"devDependencies": {
"@rtk-query/codegen-openapi": "1.2.0",
"@types/jest": "29.5.11",
"@types/ramda": "0.29.9",
"@types/react": "18.2.45",
"@types/react-dom": "18.2.18",
"@types/react-redux": "7.1.33",
"@types/yup": "0.29.14",
"@typescript-eslint/eslint-plugin": "6.15.0",
"@typescript-eslint/parser": "6.15.0",
"autoprefixer": "10.4.16",
"css-loader": "^6.8.1",
"process": "0.11.10",
"dotenv": "16.3.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-config-next": "14.0.4",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-typescript": "0.14.0",
"eslint-plugin-typescript-sort-keys": "3.1.0",
"jest": "29.7.0",
"postcss": "8.4.32",
"postcss-loader": "7.3.3",
"prettier": "3.1.1",
"tailwindcss": "3.4.0",
"ts-node": "10.9.2",
"ts-jest": "29.1.1"
},
"peerDependencies": {}
}

2778
source/ui/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
const configPath = require.resolve('./src/styles/tailwind.config.ts');
module.exports = {
plugins: ['postcss-import', 'tailwindcss/nesting', ['tailwindcss', configPath], 'autoprefixer'],
};

View file

@ -1,82 +0,0 @@
require('ts-node/register');
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const __basedir = path.resolve(__dirname, '..');
module.exports = (env, args, myEnv) => ({
entry: {
app: [path.resolve(__basedir, 'src/index.tsx')],
style: [path.resolve(__basedir, 'src/styles/global.css')],
},
output: {
path: path.resolve(__basedir, 'build'),
publicPath: '/',
},
resolve: {
alias: {
api: path.resolve(__basedir, 'src/api'),
app: path.resolve(__basedir, 'src/app'),
components: path.resolve(__basedir, 'src/components'),
constants: path.resolve(__basedir, 'src/constants'),
features: path.resolve(__basedir, 'src/features'),
hooks: path.resolve(__basedir, 'src/hooks'),
localization: path.resolve(__basedir, 'src/localization'),
pages: path.resolve(__basedir, 'src/pages'),
utils: path.resolve(__basedir, 'src/utils'),
types: path.resolve(__basedir, 'src/types'),
},
extensions: ['.ts', '.tsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
},
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: path.resolve(__basedir, 'src/styles/postcss.config.js'),
},
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new HtmlWebPackPlugin({
hash: true,
filename: 'index.html', //target html
template: path.resolve(__basedir, 'src/app/assets/html/index.ejs'), //source html
env: {
websiteId: process.env.WA_WEBSITE_ID,
},
}),
new CopyPlugin({
patterns: [
{ from: path.resolve(__basedir, 'src/app/assets/public'), to: './' },
],
}),
new webpack.EnvironmentPlugin(myEnv),
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new CaseSensitivePathsPlugin(),
],
});

View file

@ -1,17 +0,0 @@
const { merge } = require('webpack-merge');
const common = require('./webpack-common');
const myEnv = require('dotenv').config({ path: 'config/app/.env.local' }).parsed;
module.exports = (env, args) => {
return merge(common(env, args, myEnv), {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
host: '0.0.0.0',
port: '3330',
hot: true,
historyApiFallback: true,
},
});
};

View file

@ -1,16 +0,0 @@
const { merge } = require('webpack-merge');
const myEnv = require('dotenv').config({ path: 'config/app/.env.prod' }).parsed;
const common = require('./webpack-common');
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = (env, args) => {
return merge(common(env, args, myEnv), {
mode: 'production',
devtool: false,
plugins: [
// new BundleAnalyzerPlugin(),
],
});
};

View file

@ -1,66 +0,0 @@
// @ts-nocheck
import React, { FC } from 'react';
import { Route, Switch } from 'wouter';
import { useAuth } from '@procyon/auth/hook/useAuth';
import Section from '@procyon/components/Section';
import { Skeleton } from '@procyon/components/Skeleton';
import { MenuItem } from '@procyon/components/Skeleton/types';
import { WelcomePage } from 'pages/WelcomePage';
import { buildRoute, Routes } from './routes';
const topMenu: MenuItem[][] = [
[
{ label: 'Routa', href: buildRoute('root') },
],
[
// authenticated
],
];
const userMenu: MenuItem[][] = [
[
// public
],
[
// authenticated
],
];
export const App: FC = () => {
const { authenticated } = useAuth();
return (
<Skeleton
items={{
logo: <>Many</>,
top: authenticated ? topMenu[1] : topMenu[0],
user: authenticated ? userMenu[1] : userMenu[0],
}}
components={{
footer: (
<div className="text-center footer">
<Section>
Tento web používá pouze technické cookie. Monitorování návštěvnosti je zcela anonymní a je prováděno na straně
provozovatele webu.
</Section>
</div>
)
}}
>
{authenticated ? (
<Switch>
<Route>404!</Route>
</Switch>
) : (
<Switch>
<Route path={Routes.root} component={WelcomePage} />
<Route>404!</Route>
</Switch>
)}
</Skeleton>
);
};

View file

@ -1,41 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css?family=Ubuntu:400,700&amp;subset=cyrillic-ext" rel="stylesheet">
<title>$(AppName)</title>
<meta name="robots" content="index,follow">
<style>
body {
padding: 0;
margin: 0;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: #ddd;
}
::-webkit-scrollbar-thumb {
background: #666;
}
</style>
</head>
<body>
<div id="app"></div>
</body>
<% if (htmlWebpackPlugin.options.env.websiteId) { %>
<script async src="https://wa.romanjaros.dev/script.js"
data-website-id="<%= htmlWebpackPlugin.options.env.websiteId %>">
</script>
<% } %>
</html>

View file

@ -0,0 +1,28 @@
// @ts-nocheck
import '../styles/global.css';
import type { Metadata } from 'next';
import { Poppins } from 'next/font/google';
import { ReactNode } from 'react';
import { ReduxProvider } from '../providers/Redux';
import { SkeletonProvider } from '../providers/Skeleton';
const inter = Poppins({ weight: ['300', '500', '700'], display: 'swap', subsets: ['latin'] });
export const metadata: Metadata = {
title: `$(AppName)`,
description: '',
};
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
return (
<ReduxProvider>
<html lang="en">
<body className={inter.className}>
<SkeletonProvider>{children}</SkeletonProvider>
</body>
</html>
</ReduxProvider>
);
}

View file

@ -0,0 +1,4 @@
// @ts-nocheck
export default function Page() {
return <b>Next page</b>;
}

View file

@ -0,0 +1,4 @@
// @ts-nocheck
export default function Page() {
return <>Welcome!</>;
}

View file

@ -1,16 +0,0 @@
// @ts-nocheck
import { DotNestedKeys } from '@procyon/localization/types';
export const Routes = {
root: '/'
};
export const buildRoute = (route: DotNestedKeys<typeof Routes>) => {
let path = '';
let obj: Record<string, any> = Routes;
route.split('.').forEach((key: any) => {
path += obj[key]?.base ?? obj[key];
obj = obj[key];
});
return path;
};

View file

@ -1,34 +0,0 @@
// @ts-nocheck
// import generated apis
import { AnyAction, combineReducers, configureStore, ThunkDispatch } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import authReducer from '@procyon/auth/slice';
import { AUTH_REDUCER_NAME } from '@procyon/auth/types';
import { ROOT_REDUCER_NAME } from '@procyon/constants/redux';
import { emptyApi } from '../api/emptyApi';
type procyonReducer = {
[AUTH_REDUCER_NAME]: typeof authReducer;
};
const store = configureStore({
reducer: {
[ROOT_REDUCER_NAME]: combineReducers<procyonReducer>({
[AUTH_REDUCER_NAME]: authReducer,
}),
[emptyApi.reducerPath]: emptyApi.reducer,
},
middleware: (gDM) => [...gDM({ serializableCheck: false }).concat([emptyApi.middleware])],
devTools: process.env.NODE_ENV === 'development'
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = ThunkDispatch<RootState, unknown, AnyAction>;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export default store;

View file

@ -1,25 +0,0 @@
// @ts-nocheck
import 'types/global';
import 'localization/locale';
import 'utils/yup';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { ModalWrapper } from '@procyon/components/Modal/context';
import { ToasterWrapper } from '@procyon/components/Toaster/context';
import { App } from './app/app';
import store from './app/store';
render(
<Provider store={store}>
<ModalWrapper>
<ToasterWrapper>
<App />
</ToasterWrapper>
</ModalWrapper>
</Provider>,
document.getElementById('app')
);

View file

@ -1,6 +0,0 @@
// @ts-nocheck
import React from 'react';
export const WelcomePage = () => {
return <div className="welcome">Seedling app generator.</div>;
};

View file

@ -0,0 +1,16 @@
// @ts-nocheck
'use client';
import { useRef, ReactNode } from 'react';
import { Provider } from 'react-redux';
import { makeStore } from '../redux/store';
export function ReduxProvider({ children }: { children: ReactNode }) {
const storeRef = useRef<ReturnType<typeof makeStore> | null>(null);
if (!storeRef.current) {
storeRef.current = makeStore();
}
return <Provider store={storeRef.current}>{children}</Provider>;
}

View file

@ -0,0 +1,28 @@
// @ts-nocheck
'use client';
import { useRouter } from 'next/navigation';
import { Skeleton } from '@procyon/components/Skeleton';
export function SkeletonProvider({ children }: Readonly<{ children: React.ReactNode }>) {
const router = useRouter();
return (
<Skeleton
onHref={(path) => router.push(path as string)}
items={{
logo: <>App</>,
top: [
{
label: 'Home',
href: '/',
},
{
label: 'Next page',
href: '/next-page',
},
],
}}>
{children}
</Skeleton>
);
}

View file

@ -0,0 +1,27 @@
// @ts-nocheck
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import authReducer from '@procyon/auth/slice';
import { emptyApi } from './emptyApi';
export const makeStore = () =>
configureStore({
reducer: {
procyon: combineReducers({
auth: authReducer,
}),
[emptyApi.reducerPath]: emptyApi.reducer,
},
middleware: (gDM) => gDM().prepend(emptyApi.middleware),
devTools: process.env.NODE_ENV === 'development',
});
export type ReduxStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<ReduxStore['getState']>;
export type AppDispatch = ReduxStore['dispatch'];
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View file

@ -1,8 +0,0 @@
const component = () => ({
});
module.exports = ({ addComponents, theme }) => {
addComponents(component(theme));
};
module.exports.component = component;

View file

@ -0,0 +1,9 @@
import { PluginCreator } from 'tailwindcss/types/config';
const component = () => ({});
const creator: PluginCreator = ({ addComponents, theme }) => {
addComponents({});
};
export default creator;

View file

@ -1,9 +1,5 @@
@import '@procyon/styles/global.css';
@import "./variables.css";
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;700&display=swap');
body {
@apply bg-white text-black text-sm;
font-family: 'Poppins', sans-serif;

View file

@ -1,4 +0,0 @@
const postcss = require('@procyon/styles/postcss.config');
const configPath = require.resolve('./tailwind.config.js');
module.exports = postcss({ tailwindConfigFile: configPath });

View file

@ -1,23 +0,0 @@
const path = require('path');
const plugin = require('tailwindcss/plugin');
module.exports = {
presets: [require('@procyon/styles/tailwind.config')],
content: [
path.resolve(__dirname + '../../**/*.{js,ts,tsx}'),
path.resolve(__dirname + '../../../node_modules/@procyon/**/*.{js,ts,tsx}'),
],
safelist: require('@procyon/styles/tailwind.config').safelist,
theme: {
extend: require('./theme'),
},
variants: {
extend: {
borderWidth: ['last'],
},
},
plugins: [
plugin(require('./components/component')),
],
};

View file

@ -0,0 +1,18 @@
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
import path from 'path';
export default {
presets: [require('@procyon/styles/tailwind.config')],
content: [
path.resolve(__dirname + '../../app/*.{js,ts,tsx}'),
path.resolve(__dirname + '../../pages/*.{js,ts,tsx}'),
path.resolve(__dirname + '../../components/*.{js,ts,tsx}'),
path.resolve(__dirname + '../../../node_modules/@procyon/**/*.{js,ts,tsx}'),
],
safelist: require('@procyon/styles/tailwind.config').safelist,
theme: {
extend: require('./theme'),
},
plugins: [plugin(require('./components/component'))],
} satisfies Config;

View file

@ -1,10 +0,0 @@
const { pallete } = require('@procyon/styles/utils/color');
module.exports = {
colors: {
disabled: pallete('disabled'),
},
borderWidth: {
1: '1px',
},
};

View file

@ -0,0 +1,12 @@
// @ts-nocheck
import palette from '@procyon/styles/palette';
module.exports = {
colors: {
...palette,
},
borderWidth: {
1: '1px',
},
};

View file

@ -1,5 +0,0 @@
:root {
--color-h: 0;
--color-s: 0%;
--color-l: 0%;
}

View file

@ -1,13 +0,0 @@
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;
}
namespace NodeJS {
interface ProcessEnv {
ENDPOINT_BASE_URL: string;
}
}
}
export default global;

5
source/ui/src/types/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

@ -1,8 +0,0 @@
// @ts-nocheck
import { StringSchema } from 'yup';
declare module 'yup' {
interface StringSchema {
// place custom Yup definition of validation
}
}

View file

@ -1,7 +1,7 @@
// @ts-nocheck
// place import for custom Yup validations
import * as Yup from 'yup';
import * as Yup from 'src/utils/yup';
Yup.setLocale({
mixed: {

View file

@ -1,9 +0,0 @@
import { Definition } from 'nightwatch';
declare module 'nightwatch' {
interface NightwatchCustomPageObjects {
}
export interface NightwatchCustomCommands {
}
}

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"outDir": ".nightwatchjs",
"baseUrl": ".",
"rootDir": ".",
"allowJs": true,
"skipLibCheck": true,
"module": "commonjs",
"strictNullChecks": true,
"strictFunctionTypes": false,
"target": "es6",
"lib": [
"es6",
],
},
"files": [
"./nightwatchjs.d.ts"
],
"include": [
"./**/*.ts",
]
}