mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Project Initialization
This commit is contained in:
35
packages/api-common/package.json
Normal file
35
packages/api-common/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@edr/api-common",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edr/types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edr/eslint-config": "workspace:*",
|
||||
"@edr/tsconfig": "workspace:*",
|
||||
"@nestjs/common": "^10.4.6",
|
||||
"@nestjs/core": "^10.4.6",
|
||||
"@types/node": "^20.14.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
11
packages/api-common/src/decorators/current-user.decorator.ts
Normal file
11
packages/api-common/src/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
// TODO: integrate @edr/auth — this decorator currently returns whatever was
|
||||
// attached to request.user by the (not-yet-wired) auth middleware. Once the
|
||||
// auth package is integrated, replace this with the real user type.
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user ?? null;
|
||||
},
|
||||
);
|
||||
8
packages/api-common/src/decorators/public.decorator.ts
Normal file
8
packages/api-common/src/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
|
||||
// TODO: integrate @edr/auth — pair with the JwtAuthGuard from the auth package
|
||||
// so it skips authentication for routes marked @Public(). Until then, this is
|
||||
// just metadata.
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
8
packages/api-common/src/decorators/roles.decorator.ts
Normal file
8
packages/api-common/src/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
|
||||
// TODO: integrate @edr/auth — pair this metadata with a RolesGuard from the
|
||||
// auth package. Until then, this decorator only sets metadata and has no
|
||||
// runtime effect.
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
20
packages/api-common/src/entities/base.entity.ts
Normal file
20
packages/api-common/src/entities/base.entity.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
|
||||
deletedAt?: Date | null;
|
||||
}
|
||||
58
packages/api-common/src/filters/http-exception.filter.ts
Normal file
58
packages/api-common/src/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
|
||||
interface ErrorResponseBody {
|
||||
success: false;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const messageRaw =
|
||||
exception instanceof HttpException ? exception.getResponse() : 'Internal server error';
|
||||
|
||||
const message =
|
||||
typeof messageRaw === 'string'
|
||||
? messageRaw
|
||||
: (messageRaw as { message?: string }).message ?? 'Unexpected error';
|
||||
|
||||
const body: ErrorResponseBody = {
|
||||
success: false,
|
||||
statusCode: status,
|
||||
message,
|
||||
error: exception instanceof Error ? exception.name : 'Error',
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
};
|
||||
|
||||
if (status >= 500) {
|
||||
this.logger.error(`${request.method} ${request.url} -> ${status}`, (exception as Error)?.stack);
|
||||
} else {
|
||||
this.logger.warn(`${request.method} ${request.url} -> ${status} ${message}`);
|
||||
}
|
||||
|
||||
response.status(status).json(body);
|
||||
}
|
||||
}
|
||||
19
packages/api-common/src/index.ts
Normal file
19
packages/api-common/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Decorators
|
||||
export * from './decorators/current-user.decorator';
|
||||
export * from './decorators/roles.decorator';
|
||||
export * from './decorators/public.decorator';
|
||||
|
||||
// Filters
|
||||
export * from './filters/http-exception.filter';
|
||||
|
||||
// Interceptors
|
||||
export * from './interceptors/response-transform.interceptor';
|
||||
|
||||
// Pipes
|
||||
export * from './pipes/validation.pipe';
|
||||
|
||||
// Entities
|
||||
export * from './entities/base.entity';
|
||||
|
||||
// Repositories
|
||||
export * from './repositories/base.repository';
|
||||
@@ -0,0 +1,22 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface StandardResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ResponseTransformInterceptor<T> implements NestInterceptor<T, StandardResponse<T>> {
|
||||
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<StandardResponse<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
10
packages/api-common/src/pipes/validation.pipe.ts
Normal file
10
packages/api-common/src/pipes/validation.pipe.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ValidationPipe, ValidationPipeOptions } from '@nestjs/common';
|
||||
|
||||
export const createValidationPipe = (overrides?: ValidationPipeOptions): ValidationPipe =>
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
transformOptions: { enableImplicitConversion: true },
|
||||
...overrides,
|
||||
});
|
||||
52
packages/api-common/src/repositories/base.repository.ts
Normal file
52
packages/api-common/src/repositories/base.repository.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere,
|
||||
ObjectLiteral,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseRepository<T extends ObjectLiteral> {
|
||||
protected constructor(protected readonly repository: Repository<T>) {}
|
||||
|
||||
/** Find a single entity by its primary key. */
|
||||
async findById(id: string, options?: Omit<FindOneOptions<T>, 'where'>): Promise<T | null> {
|
||||
return this.repository.findOne({
|
||||
...options,
|
||||
where: { id } as unknown as FindOptionsWhere<T>,
|
||||
});
|
||||
}
|
||||
|
||||
/** Find many entities matching the given options. */
|
||||
async findAll(options?: FindManyOptions<T>): Promise<T[]> {
|
||||
return this.repository.find(options);
|
||||
}
|
||||
|
||||
/** Find many entities + return the total count for pagination. */
|
||||
async findAndCount(options?: FindManyOptions<T>): Promise<[T[], number]> {
|
||||
return this.repository.findAndCount(options);
|
||||
}
|
||||
|
||||
/** Create and persist a new entity. */
|
||||
async create(data: DeepPartial<T>): Promise<T> {
|
||||
const entity = this.repository.create(data);
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
/** Patch an entity in place and return the reloaded row. */
|
||||
async update(id: string, data: DeepPartial<T>): Promise<T | null> {
|
||||
await this.repository.update(id, data as never);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/** Soft-delete an entity by primary key (sets deleted_at). */
|
||||
async softDelete(id: string): Promise<void> {
|
||||
await this.repository.softDelete(id);
|
||||
}
|
||||
|
||||
/** Permanently delete an entity. Avoid in domain code; prefer softDelete. */
|
||||
async hardDelete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
}
|
||||
9
packages/api-common/tsconfig.json
Normal file
9
packages/api-common/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/nestjs.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
1
packages/api-common/tsconfig.tsbuildinfo
Normal file
1
packages/api-common/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
44
packages/config/eslint-config/base.js
Normal file
44
packages/config/eslint-config/base.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc' },
|
||||
},
|
||||
],
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
ignorePatterns: ['dist', 'build', '.turbo', 'node_modules', 'coverage'],
|
||||
};
|
||||
13
packages/config/eslint-config/nestjs.js
Normal file
13
packages/config/eslint-config/nestjs.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ['./base.js'],
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
};
|
||||
17
packages/config/eslint-config/package.json
Normal file
17
packages/config/eslint-config/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@edr/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "base.js",
|
||||
"files": ["base.js", "nestjs.js", "react.js"],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.0",
|
||||
"@typescript-eslint/parser": "^8.8.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.12"
|
||||
}
|
||||
}
|
||||
20
packages/config/eslint-config/react.js
vendored
Normal file
20
packages/config/eslint-config/react.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'./base.js',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
plugins: ['react-refresh'],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
};
|
||||
10
packages/config/prettier-config/index.js
Normal file
10
packages/config/prettier-config/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
arrowParens: 'always',
|
||||
endOfLine: 'lf',
|
||||
};
|
||||
6
packages/config/prettier-config/package.json
Normal file
6
packages/config/prettier-config/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@edr/prettier-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "index.js"
|
||||
}
|
||||
24
packages/config/tsconfig/base.json
Normal file
24
packages/config/tsconfig/base.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
16
packages/config/tsconfig/nestjs.json
Normal file
16
packages/config/tsconfig/nestjs.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": false,
|
||||
"isolatedModules": false,
|
||||
"incremental": true,
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
6
packages/config/tsconfig/package.json
Normal file
6
packages/config/tsconfig/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@edr/tsconfig",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"files": ["base.json", "nestjs.json", "react.json"]
|
||||
}
|
||||
14
packages/config/tsconfig/react.json
Normal file
14
packages/config/tsconfig/react.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
23
packages/types/package.json
Normal file
23
packages/types/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@edr/types",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./common": "./src/common/index.ts",
|
||||
"./freight": "./src/freight/index.ts",
|
||||
"./passenger": "./src/passenger/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edr/eslint-config": "workspace:*",
|
||||
"@edr/tsconfig": "workspace:*",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
34
packages/types/src/common/index.ts
Normal file
34
packages/types/src/common/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface PaginationMeta {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
meta: PaginationMeta;
|
||||
}
|
||||
|
||||
export interface PaginationQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
}
|
||||
96
packages/types/src/freight/index.ts
Normal file
96
packages/types/src/freight/index.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { BaseEntity } from '../common';
|
||||
|
||||
export enum BookingStatus {
|
||||
Draft = 'DRAFT',
|
||||
Confirmed = 'CONFIRMED',
|
||||
InTransit = 'IN_TRANSIT',
|
||||
Delivered = 'DELIVERED',
|
||||
Cancelled = 'CANCELLED',
|
||||
}
|
||||
|
||||
export enum ConsignmentStatus {
|
||||
Pending = 'PENDING',
|
||||
Loaded = 'LOADED',
|
||||
InTransit = 'IN_TRANSIT',
|
||||
AtDestination = 'AT_DESTINATION',
|
||||
Delivered = 'DELIVERED',
|
||||
Returned = 'RETURNED',
|
||||
}
|
||||
|
||||
export enum TrainStatus {
|
||||
Available = 'AVAILABLE',
|
||||
Scheduled = 'SCHEDULED',
|
||||
InService = 'IN_SERVICE',
|
||||
UnderMaintenance = 'UNDER_MAINTENANCE',
|
||||
OutOfService = 'OUT_OF_SERVICE',
|
||||
}
|
||||
|
||||
export enum CargoType {
|
||||
Container = 'CONTAINER',
|
||||
BulkLiquid = 'BULK_LIQUID',
|
||||
BulkDry = 'BULK_DRY',
|
||||
General = 'GENERAL',
|
||||
Refrigerated = 'REFRIGERATED',
|
||||
Hazardous = 'HAZARDOUS',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
Pending = 'PENDING',
|
||||
Paid = 'PAID',
|
||||
Overdue = 'OVERDUE',
|
||||
Cancelled = 'CANCELLED',
|
||||
Refunded = 'REFUNDED',
|
||||
}
|
||||
|
||||
export interface ICustomer extends BaseEntity {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address?: string;
|
||||
taxId?: string;
|
||||
}
|
||||
|
||||
export interface ITrain extends BaseEntity {
|
||||
code: string;
|
||||
capacityTons: number;
|
||||
status: TrainStatus;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ITrackingEvent extends BaseEntity {
|
||||
consignmentId: string;
|
||||
location: string;
|
||||
status: ConsignmentStatus;
|
||||
occurredAt: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface IConsignment extends BaseEntity {
|
||||
bookingId: string;
|
||||
trackingNumber: string;
|
||||
cargoType: CargoType;
|
||||
weightKg: number;
|
||||
status: ConsignmentStatus;
|
||||
originStation: string;
|
||||
destinationStation: string;
|
||||
}
|
||||
|
||||
export interface IBooking extends BaseEntity {
|
||||
reference: string;
|
||||
customerId: string;
|
||||
trainId?: string;
|
||||
status: BookingStatus;
|
||||
scheduledDate: string;
|
||||
totalAmount: number;
|
||||
paymentStatus: PaymentStatus;
|
||||
}
|
||||
|
||||
export interface IInvoice extends BaseEntity {
|
||||
bookingId: string;
|
||||
invoiceNumber: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: PaymentStatus;
|
||||
issuedAt: string;
|
||||
dueAt: string;
|
||||
}
|
||||
3
packages/types/src/index.ts
Normal file
3
packages/types/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './common';
|
||||
export * as Freight from './freight';
|
||||
export * as Passenger from './passenger';
|
||||
95
packages/types/src/passenger/index.ts
Normal file
95
packages/types/src/passenger/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { BaseEntity } from '../common';
|
||||
|
||||
export enum TicketStatus {
|
||||
Reserved = 'RESERVED',
|
||||
Confirmed = 'CONFIRMED',
|
||||
CheckedIn = 'CHECKED_IN',
|
||||
Cancelled = 'CANCELLED',
|
||||
Refunded = 'REFUNDED',
|
||||
Expired = 'EXPIRED',
|
||||
}
|
||||
|
||||
export enum SeatClass {
|
||||
Economy = 'ECONOMY',
|
||||
Business = 'BUSINESS',
|
||||
First = 'FIRST',
|
||||
Sleeper = 'SLEEPER',
|
||||
}
|
||||
|
||||
export enum SeatStatus {
|
||||
Available = 'AVAILABLE',
|
||||
Reserved = 'RESERVED',
|
||||
Occupied = 'OCCUPIED',
|
||||
Blocked = 'BLOCKED',
|
||||
}
|
||||
|
||||
export enum ScheduleStatus {
|
||||
Scheduled = 'SCHEDULED',
|
||||
Boarding = 'BOARDING',
|
||||
Departed = 'DEPARTED',
|
||||
Arrived = 'ARRIVED',
|
||||
Cancelled = 'CANCELLED',
|
||||
Delayed = 'DELAYED',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
Pending = 'PENDING',
|
||||
Paid = 'PAID',
|
||||
Failed = 'FAILED',
|
||||
Refunded = 'REFUNDED',
|
||||
}
|
||||
|
||||
export interface IStation extends BaseEntity {
|
||||
code: string;
|
||||
name: string;
|
||||
city: string;
|
||||
country: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
}
|
||||
|
||||
export interface ISchedule extends BaseEntity {
|
||||
trainCode: string;
|
||||
originStationId: string;
|
||||
destinationStationId: string;
|
||||
departureTime: string;
|
||||
arrivalTime: string;
|
||||
status: ScheduleStatus;
|
||||
basePrice: number;
|
||||
}
|
||||
|
||||
export interface ISeat extends BaseEntity {
|
||||
scheduleId: string;
|
||||
seatNumber: string;
|
||||
seatClass: SeatClass;
|
||||
status: SeatStatus;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface IPassenger extends BaseEntity {
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
nationalId?: string;
|
||||
dateOfBirth?: string;
|
||||
}
|
||||
|
||||
export interface ITicket extends BaseEntity {
|
||||
reference: string;
|
||||
passengerId: string;
|
||||
scheduleId: string;
|
||||
seatId: string;
|
||||
status: TicketStatus;
|
||||
pricePaid: number;
|
||||
issuedAt: string;
|
||||
}
|
||||
|
||||
export interface IPayment extends BaseEntity {
|
||||
ticketId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: PaymentStatus;
|
||||
provider: string;
|
||||
providerTransactionId?: string;
|
||||
paidAt?: string;
|
||||
}
|
||||
9
packages/types/tsconfig.json
Normal file
9
packages/types/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
32
packages/ui-common/package.json
Normal file
32
packages/ui-common/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@edr/ui-common",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edr/types": "workspace:*",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edr/eslint-config": "workspace:*",
|
||||
"@edr/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
31
packages/ui-common/src/components/Badge/Badge.tsx
Normal file
31
packages/ui-common/src/components/Badge/Badge.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type BadgeTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
|
||||
|
||||
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
tone?: BadgeTone;
|
||||
}
|
||||
|
||||
const toneClasses: Record<BadgeTone, string> = {
|
||||
neutral: 'bg-gray-100 text-gray-700',
|
||||
success: 'bg-green-100 text-green-700',
|
||||
warning: 'bg-amber-100 text-amber-800',
|
||||
danger: 'bg-red-100 text-red-700',
|
||||
info: 'bg-sky-100 text-sky-700',
|
||||
};
|
||||
|
||||
const Badge = ({ tone = 'neutral', className, children, ...rest }: BadgeProps) => (
|
||||
<span
|
||||
className={clsx(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
|
||||
toneClasses[tone],
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export default Badge;
|
||||
2
packages/ui-common/src/components/Badge/index.ts
Normal file
2
packages/ui-common/src/components/Badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './Badge';
|
||||
export type { BadgeProps, BadgeTone } from './Badge';
|
||||
46
packages/ui-common/src/components/Button/Button.tsx
Normal file
46
packages/ui-common/src/components/Button/Button.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
export type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-300',
|
||||
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 disabled:bg-gray-50',
|
||||
ghost: 'bg-transparent text-gray-900 hover:bg-gray-100',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 disabled:bg-red-300',
|
||||
};
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-base',
|
||||
lg: 'px-6 py-3 text-lg',
|
||||
};
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'primary', size = 'md', isLoading, disabled, className, children, ...rest }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={disabled || isLoading}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors disabled:cursor-not-allowed',
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{isLoading ? 'Loading...' : children}
|
||||
</button>
|
||||
),
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export default Button;
|
||||
2
packages/ui-common/src/components/Button/index.ts
Normal file
2
packages/ui-common/src/components/Button/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './Button';
|
||||
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
|
||||
38
packages/ui-common/src/components/Form/FormField.tsx
Normal file
38
packages/ui-common/src/components/Form/FormField.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { InputHTMLAttributes, ReactNode, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface FormFieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
error?: string;
|
||||
hint?: ReactNode;
|
||||
}
|
||||
|
||||
const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
||||
({ label, error, hint, id, className, ...rest }, ref) => {
|
||||
const fieldId = id ?? rest.name;
|
||||
return (
|
||||
<label className="flex flex-col gap-1 text-sm" htmlFor={fieldId}>
|
||||
<span className="font-medium text-gray-700">{label}</span>
|
||||
<input
|
||||
ref={ref}
|
||||
id={fieldId}
|
||||
aria-invalid={Boolean(error)}
|
||||
className={clsx(
|
||||
'rounded-md border px-3 py-2 text-gray-900 outline-none transition focus:ring-2',
|
||||
error
|
||||
? 'border-red-400 focus:border-red-500 focus:ring-red-100'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-100',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
{hint && !error ? <span className="text-xs text-gray-500">{hint}</span> : null}
|
||||
{error ? <span className="text-xs text-red-600">{error}</span> : null}
|
||||
</label>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FormField.displayName = 'FormField';
|
||||
|
||||
export default FormField;
|
||||
2
packages/ui-common/src/components/Form/index.ts
Normal file
2
packages/ui-common/src/components/Form/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as FormField } from './FormField';
|
||||
export type { FormFieldProps } from './FormField';
|
||||
38
packages/ui-common/src/components/Layout/DashboardLayout.tsx
Normal file
38
packages/ui-common/src/components/Layout/DashboardLayout.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Sidebar, { SidebarItem } from './Sidebar';
|
||||
|
||||
export interface DashboardLayoutProps {
|
||||
title?: string;
|
||||
sidebarItems: SidebarItem[];
|
||||
activeHref?: string;
|
||||
onNavigate?: (href: string) => void;
|
||||
headerRight?: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const DashboardLayout = ({
|
||||
title,
|
||||
sidebarItems,
|
||||
activeHref,
|
||||
onNavigate,
|
||||
headerRight,
|
||||
children,
|
||||
}: DashboardLayoutProps) => (
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
<Sidebar
|
||||
title={title}
|
||||
items={sidebarItems}
|
||||
activeHref={activeHref}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<header className="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-3">
|
||||
<div className="text-sm font-medium text-gray-700">{title}</div>
|
||||
<div>{headerRight}</div>
|
||||
</header>
|
||||
<main className="flex-1 overflow-auto p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DashboardLayout;
|
||||
46
packages/ui-common/src/components/Layout/Sidebar.tsx
Normal file
46
packages/ui-common/src/components/Layout/Sidebar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface SidebarItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
title?: string;
|
||||
items: SidebarItem[];
|
||||
activeHref?: string;
|
||||
onNavigate?: (href: string) => void;
|
||||
}
|
||||
|
||||
const Sidebar = ({ title, items, activeHref, onNavigate }: SidebarProps) => (
|
||||
<aside className="flex w-60 flex-col gap-1 border-r border-gray-200 bg-white px-3 py-4">
|
||||
{title ? <div className="px-2 pb-3 text-sm font-semibold text-gray-700">{title}</div> : null}
|
||||
<nav className="flex flex-col gap-0.5">
|
||||
{items.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={(event) => {
|
||||
if (onNavigate) {
|
||||
event.preventDefault();
|
||||
onNavigate(item.href);
|
||||
}
|
||||
}}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors',
|
||||
activeHref === item.href
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 hover:bg-gray-100',
|
||||
)}
|
||||
>
|
||||
{item.icon ? <span className="text-gray-500">{item.icon}</span> : null}
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
|
||||
export default Sidebar;
|
||||
4
packages/ui-common/src/components/Layout/index.ts
Normal file
4
packages/ui-common/src/components/Layout/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Sidebar } from './Sidebar';
|
||||
export { default as DashboardLayout } from './DashboardLayout';
|
||||
export type { SidebarProps, SidebarItem } from './Sidebar';
|
||||
export type { DashboardLayoutProps } from './DashboardLayout';
|
||||
48
packages/ui-common/src/components/Modal/Modal.tsx
Normal file
48
packages/ui-common/src/components/Modal/Modal.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
|
||||
export interface ModalProps {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
const Modal = ({ open, title, onClose, children, footer }: ModalProps) => {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-lg rounded-lg bg-white shadow-xl"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{title ? (
|
||||
<div className="border-b border-gray-200 px-4 py-3 text-base font-semibold text-gray-900">
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="px-4 py-4 text-sm text-gray-700">{children}</div>
|
||||
{footer ? (
|
||||
<div className="flex justify-end gap-2 border-t border-gray-200 px-4 py-3">{footer}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
2
packages/ui-common/src/components/Modal/index.ts
Normal file
2
packages/ui-common/src/components/Modal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './Modal';
|
||||
export type { ModalProps } from './Modal';
|
||||
60
packages/ui-common/src/components/Table/Table.tsx
Normal file
60
packages/ui-common/src/components/Table/Table.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface TableColumn<T> {
|
||||
key: string;
|
||||
header: string;
|
||||
render?: (row: T) => ReactNode;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
export interface TableProps<T> {
|
||||
columns: TableColumn<T>[];
|
||||
data: T[];
|
||||
rowKey: (row: T) => string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
function Table<T>({ columns, data, rowKey, emptyMessage = 'No data' }: TableProps<T>) {
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-md border border-gray-200">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
style={column.width ? { width: column.width } : undefined}
|
||||
className="px-4 py-2 text-left font-medium text-gray-700"
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-4 py-6 text-center text-gray-500">
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row) => (
|
||||
<tr key={rowKey(row)} className="hover:bg-gray-50">
|
||||
{columns.map((column) => (
|
||||
<td key={column.key} className="px-4 py-2 text-gray-900">
|
||||
{column.render
|
||||
? column.render(row)
|
||||
: ((row as unknown as Record<string, ReactNode>)[column.key] ?? '-')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Table;
|
||||
2
packages/ui-common/src/components/Table/index.ts
Normal file
2
packages/ui-common/src/components/Table/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './Table';
|
||||
export type { TableProps, TableColumn } from './Table';
|
||||
26
packages/ui-common/src/index.ts
Normal file
26
packages/ui-common/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export { default as Button } from './components/Button';
|
||||
export type { ButtonProps, ButtonVariant, ButtonSize } from './components/Button';
|
||||
|
||||
export { default as Table } from './components/Table';
|
||||
export type { TableProps, TableColumn } from './components/Table';
|
||||
|
||||
export { FormField } from './components/Form';
|
||||
export type { FormFieldProps } from './components/Form';
|
||||
|
||||
export { default as Modal } from './components/Modal';
|
||||
export type { ModalProps } from './components/Modal';
|
||||
|
||||
export { default as Badge } from './components/Badge';
|
||||
export type { BadgeProps, BadgeTone } from './components/Badge';
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
DashboardLayout,
|
||||
} from './components/Layout';
|
||||
export type {
|
||||
SidebarProps,
|
||||
SidebarItem,
|
||||
DashboardLayoutProps,
|
||||
} from './components/Layout';
|
||||
|
||||
export * from './theme';
|
||||
24
packages/ui-common/src/theme/colors.ts
Normal file
24
packages/ui-common/src/theme/colors.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const colors = {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
neutral: {
|
||||
50: '#f9fafb',
|
||||
100: '#f3f4f6',
|
||||
200: '#e5e7eb',
|
||||
400: '#9ca3af',
|
||||
600: '#4b5563',
|
||||
900: '#111827',
|
||||
},
|
||||
success: '#16a34a',
|
||||
warning: '#f59e0b',
|
||||
danger: '#dc2626',
|
||||
info: '#0ea5e9',
|
||||
} as const;
|
||||
|
||||
export type Colors = typeof colors;
|
||||
2
packages/ui-common/src/theme/index.ts
Normal file
2
packages/ui-common/src/theme/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './colors';
|
||||
export * from './typography';
|
||||
28
packages/ui-common/src/theme/typography.ts
Normal file
28
packages/ui-common/src/theme/typography.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const typography = {
|
||||
fontFamily: {
|
||||
sans: '"Inter", "Segoe UI", sans-serif',
|
||||
mono: '"Fira Code", "Menlo", monospace',
|
||||
},
|
||||
fontSize: {
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
base: '1rem',
|
||||
lg: '1.125rem',
|
||||
xl: '1.25rem',
|
||||
'2xl': '1.5rem',
|
||||
'3xl': '1.875rem',
|
||||
},
|
||||
fontWeight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
semibold: 600,
|
||||
bold: 700,
|
||||
},
|
||||
lineHeight: {
|
||||
tight: 1.25,
|
||||
normal: 1.5,
|
||||
relaxed: 1.75,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Typography = typeof typography;
|
||||
8
packages/ui-common/tsconfig.json
Normal file
8
packages/ui-common/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/react.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user