Initial commit: Sase.tr VIN Sorgulama Platformu
Features: - Next.js 16.1.3 frontend with Turbopack - NestJS API with Prisma ORM - EMEX VIN scraper integration - Turkish translations for automotive parts - JWT authentication with refresh tokens - PM2 production deployment Tech Stack: - Frontend: Next.js 16.1, React 19, TailwindCSS - Backend: NestJS, Prisma, MySQL - Scraping: Puppeteer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
41
packages/shared/package.json
Normal file
41
packages/shared/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@sase/shared",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/types/index.js"
|
||||
},
|
||||
"./constants": {
|
||||
"types": "./dist/constants/index.d.ts",
|
||||
"default": "./dist/constants/index.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"default": "./dist/utils/index.js"
|
||||
},
|
||||
"./schemas": {
|
||||
"types": "./dist/schemas/index.d.ts",
|
||||
"default": "./dist/schemas/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint src/",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
76
packages/shared/src/constants/error-codes.ts
Normal file
76
packages/shared/src/constants/error-codes.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export const ERROR_CODES = {
|
||||
// Auth errors
|
||||
AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
|
||||
AUTH_EMAIL_EXISTS: 'AUTH_EMAIL_EXISTS',
|
||||
AUTH_USER_NOT_FOUND: 'AUTH_USER_NOT_FOUND',
|
||||
AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
|
||||
AUTH_TOKEN_INVALID: 'AUTH_TOKEN_INVALID',
|
||||
AUTH_UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
|
||||
|
||||
// Subscription errors
|
||||
SUBSCRIPTION_NOT_FOUND: 'SUBSCRIPTION_NOT_FOUND',
|
||||
SUBSCRIPTION_EXPIRED: 'SUBSCRIPTION_EXPIRED',
|
||||
SUBSCRIPTION_INACTIVE: 'SUBSCRIPTION_INACTIVE',
|
||||
SUBSCRIPTION_BRAND_LIMIT_EXCEEDED: 'SUBSCRIPTION_BRAND_LIMIT_EXCEEDED',
|
||||
|
||||
// Brand errors
|
||||
BRAND_ACCESS_DENIED: 'BRAND_ACCESS_DENIED',
|
||||
BRAND_NOT_FOUND: 'BRAND_NOT_FOUND',
|
||||
BRAND_SELECTION_REQUIRED: 'BRAND_SELECTION_REQUIRED',
|
||||
|
||||
// Vehicle errors
|
||||
VEHICLE_NOT_FOUND: 'VEHICLE_NOT_FOUND',
|
||||
VIN_INVALID: 'VIN_INVALID',
|
||||
VIN_DECODE_FAILED: 'VIN_DECODE_FAILED',
|
||||
|
||||
// Part errors
|
||||
PART_NOT_FOUND: 'PART_NOT_FOUND',
|
||||
CATEGORY_NOT_FOUND: 'CATEGORY_NOT_FOUND',
|
||||
|
||||
// Payment errors
|
||||
PAYMENT_FAILED: 'PAYMENT_FAILED',
|
||||
PAYMENT_CANCELLED: 'PAYMENT_CANCELLED',
|
||||
PAYMENT_INVALID_CARD: 'PAYMENT_INVALID_CARD',
|
||||
|
||||
// General errors
|
||||
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||
|
||||
export const ERROR_MESSAGES: Record<ErrorCode, string> = {
|
||||
[ERROR_CODES.AUTH_INVALID_CREDENTIALS]: 'Email veya sifre hatali',
|
||||
[ERROR_CODES.AUTH_EMAIL_EXISTS]: 'Bu email adresi zaten kayitli',
|
||||
[ERROR_CODES.AUTH_USER_NOT_FOUND]: 'Kullanici bulunamadi',
|
||||
[ERROR_CODES.AUTH_TOKEN_EXPIRED]: 'Oturum suresi dolmus',
|
||||
[ERROR_CODES.AUTH_TOKEN_INVALID]: 'Gecersiz oturum',
|
||||
[ERROR_CODES.AUTH_UNAUTHORIZED]: 'Bu islemi yapmaya yetkiniz yok',
|
||||
|
||||
[ERROR_CODES.SUBSCRIPTION_NOT_FOUND]: 'Abonelik bulunamadi',
|
||||
[ERROR_CODES.SUBSCRIPTION_EXPIRED]: 'Abonelik suresi dolmus',
|
||||
[ERROR_CODES.SUBSCRIPTION_INACTIVE]: 'Aktif aboneliginiz bulunmuyor',
|
||||
[ERROR_CODES.SUBSCRIPTION_BRAND_LIMIT_EXCEEDED]: 'Marka limiti asildi',
|
||||
|
||||
[ERROR_CODES.BRAND_ACCESS_DENIED]: 'Bu markaya erisiniz bulunmuyor',
|
||||
[ERROR_CODES.BRAND_NOT_FOUND]: 'Marka bulunamadi',
|
||||
[ERROR_CODES.BRAND_SELECTION_REQUIRED]: 'Lutfen marka seciniz',
|
||||
|
||||
[ERROR_CODES.VEHICLE_NOT_FOUND]: 'Arac bulunamadi',
|
||||
[ERROR_CODES.VIN_INVALID]: 'Gecersiz VIN numarasi',
|
||||
[ERROR_CODES.VIN_DECODE_FAILED]: 'VIN cozumlenemedi',
|
||||
|
||||
[ERROR_CODES.PART_NOT_FOUND]: 'Parca bulunamadi',
|
||||
[ERROR_CODES.CATEGORY_NOT_FOUND]: 'Kategori bulunamadi',
|
||||
|
||||
[ERROR_CODES.PAYMENT_FAILED]: 'Odeme basarisiz',
|
||||
[ERROR_CODES.PAYMENT_CANCELLED]: 'Odeme iptal edildi',
|
||||
[ERROR_CODES.PAYMENT_INVALID_CARD]: 'Gecersiz kart bilgileri',
|
||||
|
||||
[ERROR_CODES.VALIDATION_ERROR]: 'Dogrulama hatasi',
|
||||
[ERROR_CODES.NOT_FOUND]: 'Kayit bulunamadi',
|
||||
[ERROR_CODES.INTERNAL_ERROR]: 'Sunucu hatasi',
|
||||
[ERROR_CODES.RATE_LIMIT_EXCEEDED]: 'Cok fazla istek gonderdiniz',
|
||||
};
|
||||
51
packages/shared/src/constants/index.ts
Normal file
51
packages/shared/src/constants/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export * from './plans';
|
||||
export * from './error-codes';
|
||||
export * from './regex';
|
||||
|
||||
// API Configuration
|
||||
export const API_VERSION = 'v1';
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const MAX_PAGE_SIZE = 100;
|
||||
|
||||
// VIN Configuration
|
||||
export const VIN_LENGTH = 17;
|
||||
|
||||
// JWT Configuration
|
||||
export const JWT_ACCESS_TOKEN_EXPIRY = '15m';
|
||||
export const JWT_REFRESH_TOKEN_EXPIRY = '7d';
|
||||
|
||||
// Rate Limiting
|
||||
export const RATE_LIMIT_TTL = 60; // seconds
|
||||
export const RATE_LIMIT_MAX = 100; // requests per TTL
|
||||
|
||||
// Cache TTL (seconds)
|
||||
export const CACHE_TTL = {
|
||||
BRANDS: 3600, // 1 hour
|
||||
PLANS: 3600, // 1 hour
|
||||
CATEGORIES: 3600, // 1 hour
|
||||
USER_SESSION: 900, // 15 minutes
|
||||
} as const;
|
||||
|
||||
// Supported brands (seed data)
|
||||
export const SUPPORTED_BRANDS = [
|
||||
{ code: 'FIAT', name: 'Fiat' },
|
||||
{ code: 'RENAULT', name: 'Renault' },
|
||||
{ code: 'VOLKSWAGEN', name: 'Volkswagen' },
|
||||
{ code: 'BMW', name: 'BMW' },
|
||||
{ code: 'MERCEDES', name: 'Mercedes-Benz' },
|
||||
{ code: 'AUDI', name: 'Audi' },
|
||||
{ code: 'TOYOTA', name: 'Toyota' },
|
||||
{ code: 'HONDA', name: 'Honda' },
|
||||
{ code: 'FORD', name: 'Ford' },
|
||||
{ code: 'OPEL', name: 'Opel' },
|
||||
{ code: 'HYUNDAI', name: 'Hyundai' },
|
||||
{ code: 'KIA', name: 'Kia' },
|
||||
{ code: 'PEUGEOT', name: 'Peugeot' },
|
||||
{ code: 'CITROEN', name: 'Citroen' },
|
||||
{ code: 'SKODA', name: 'Skoda' },
|
||||
{ code: 'SEAT', name: 'SEAT' },
|
||||
{ code: 'NISSAN', name: 'Nissan' },
|
||||
{ code: 'MAZDA', name: 'Mazda' },
|
||||
{ code: 'VOLVO', name: 'Volvo' },
|
||||
{ code: 'DACIA', name: 'Dacia' },
|
||||
] as const;
|
||||
43
packages/shared/src/constants/plans.ts
Normal file
43
packages/shared/src/constants/plans.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export const PLAN_SLUGS = {
|
||||
STARTER: 'starter',
|
||||
PRO: 'pro',
|
||||
BUSINESS: 'business',
|
||||
FULL: 'full',
|
||||
} as const;
|
||||
|
||||
export type PlanSlug = (typeof PLAN_SLUGS)[keyof typeof PLAN_SLUGS];
|
||||
|
||||
export const PLAN_CONFIGS = {
|
||||
[PLAN_SLUGS.STARTER]: {
|
||||
name: 'Baslangic',
|
||||
price: 299,
|
||||
brandLimit: 1,
|
||||
hasFullAccess: false,
|
||||
features: ['1 marka erisimi', 'Sinirsiz sorgu', 'Email destek'],
|
||||
},
|
||||
[PLAN_SLUGS.PRO]: {
|
||||
name: 'Pro',
|
||||
price: 599,
|
||||
brandLimit: 3,
|
||||
hasFullAccess: false,
|
||||
features: ['3 marka erisimi', 'Sinirsiz sorgu', 'Oncelikli destek'],
|
||||
isPopular: true,
|
||||
},
|
||||
[PLAN_SLUGS.BUSINESS]: {
|
||||
name: 'Isletme',
|
||||
price: 999,
|
||||
brandLimit: 10,
|
||||
hasFullAccess: false,
|
||||
features: ['10 marka erisimi', 'Sinirsiz sorgu', 'Telefon destek', 'API erisimi'],
|
||||
},
|
||||
[PLAN_SLUGS.FULL]: {
|
||||
name: 'Full',
|
||||
price: 1999,
|
||||
brandLimit: 0,
|
||||
hasFullAccess: true,
|
||||
features: ['Tum markalar', 'Sinirsiz sorgu', '7/24 destek', 'API erisimi', 'Ozel entegrasyon'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_PLAN_DURATION_DAYS = 30;
|
||||
export const DEFAULT_CURRENCY = 'TRY';
|
||||
20
packages/shared/src/constants/regex.ts
Normal file
20
packages/shared/src/constants/regex.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// VIN: 17 characters, excluding I, O, Q
|
||||
export const VIN_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/i;
|
||||
|
||||
// Email validation
|
||||
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// Turkish phone number
|
||||
export const PHONE_REGEX = /^(\+90|0)?[0-9]{10}$/;
|
||||
|
||||
// Turkish identity number (TC Kimlik No)
|
||||
export const TC_KIMLIK_REGEX = /^[1-9][0-9]{10}$/;
|
||||
|
||||
// OEM Part code (alphanumeric, hyphens allowed)
|
||||
export const OEM_CODE_REGEX = /^[A-Z0-9][A-Z0-9\-]{2,30}$/i;
|
||||
|
||||
// Slug format
|
||||
export const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
// Password: min 8 chars, at least 1 letter and 1 number
|
||||
export const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/;
|
||||
11
packages/shared/src/index.ts
Normal file
11
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Constants
|
||||
export * from './constants';
|
||||
|
||||
// Utils
|
||||
export * from './utils';
|
||||
|
||||
// Schemas
|
||||
export * from './schemas';
|
||||
30
packages/shared/src/schemas/index.ts
Normal file
30
packages/shared/src/schemas/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export * from './vin.schema';
|
||||
export * from './user.schema';
|
||||
export * from './subscription.schema';
|
||||
export * from './payment.schema';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
// Pagination schema
|
||||
export const paginationSchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||
sortBy: z.string().optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
||||
});
|
||||
|
||||
export type PaginationInput = z.infer<typeof paginationSchema>;
|
||||
|
||||
// ID param schema
|
||||
export const idParamSchema = z.object({
|
||||
id: z.string().cuid('Gecersiz ID'),
|
||||
});
|
||||
|
||||
export type IdParamInput = z.infer<typeof idParamSchema>;
|
||||
|
||||
// Search schema
|
||||
export const searchSchema = z.object({
|
||||
q: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
export type SearchInput = z.infer<typeof searchSchema>;
|
||||
41
packages/shared/src/schemas/payment.schema.ts
Normal file
41
packages/shared/src/schemas/payment.schema.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
import { TC_KIMLIK_REGEX, PHONE_REGEX } from '../constants';
|
||||
|
||||
export const cardSchema = z.object({
|
||||
cardHolderName: z.string().min(3, 'Kart sahibi adi gerekli'),
|
||||
cardNumber: z
|
||||
.string()
|
||||
.regex(/^\d{16}$/, 'Kart numarasi 16 haneli olmalidir')
|
||||
.transform((val) => val.replace(/\s/g, '')),
|
||||
expireMonth: z.string().regex(/^(0[1-9]|1[0-2])$/, 'Gecersiz ay'),
|
||||
expireYear: z.string().regex(/^\d{2}$/, 'Gecersiz yil'),
|
||||
cvc: z.string().regex(/^\d{3,4}$/, 'Gecersiz CVC'),
|
||||
});
|
||||
|
||||
export const buyerSchema = z.object({
|
||||
name: z.string().min(2, 'Ad gerekli'),
|
||||
surname: z.string().min(2, 'Soyad gerekli'),
|
||||
phone: z.string().regex(PHONE_REGEX, 'Gecersiz telefon numarasi'),
|
||||
identityNumber: z.string().regex(TC_KIMLIK_REGEX, 'Gecersiz TC Kimlik numarasi'),
|
||||
email: z.string().email('Gecersiz email'),
|
||||
address: z.string().min(10, 'Adres en az 10 karakter olmalidir'),
|
||||
city: z.string().min(2, 'Sehir gerekli'),
|
||||
country: z.string().default('Turkey'),
|
||||
});
|
||||
|
||||
export const initializePaymentSchema = z.object({
|
||||
planId: z.string().cuid('Gecersiz plan'),
|
||||
brandIds: z.array(z.string().cuid()).min(1, 'En az bir marka secmelisiniz'),
|
||||
card: cardSchema,
|
||||
buyer: buyerSchema,
|
||||
});
|
||||
|
||||
export const paymentCallbackSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
conversationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type CardInput = z.infer<typeof cardSchema>;
|
||||
export type BuyerInput = z.infer<typeof buyerSchema>;
|
||||
export type InitializePaymentInput = z.infer<typeof initializePaymentSchema>;
|
||||
export type PaymentCallbackInput = z.infer<typeof paymentCallbackSchema>;
|
||||
19
packages/shared/src/schemas/subscription.schema.ts
Normal file
19
packages/shared/src/schemas/subscription.schema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const selectBrandsSchema = z.object({
|
||||
brandIds: z.array(z.string().cuid()).min(1, 'En az bir marka secmelisiniz'),
|
||||
});
|
||||
|
||||
export const createSubscriptionSchema = z.object({
|
||||
planId: z.string().cuid('Gecersiz plan'),
|
||||
brandIds: z.array(z.string().cuid()).min(1, 'En az bir marka secmelisiniz'),
|
||||
});
|
||||
|
||||
export const updateSubscriptionSchema = z.object({
|
||||
planId: z.string().cuid('Gecersiz plan').optional(),
|
||||
brandIds: z.array(z.string().cuid()).optional(),
|
||||
});
|
||||
|
||||
export type SelectBrandsInput = z.infer<typeof selectBrandsSchema>;
|
||||
export type CreateSubscriptionInput = z.infer<typeof createSubscriptionSchema>;
|
||||
export type UpdateSubscriptionInput = z.infer<typeof updateSubscriptionSchema>;
|
||||
40
packages/shared/src/schemas/user.schema.ts
Normal file
40
packages/shared/src/schemas/user.schema.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod';
|
||||
import { EMAIL_REGEX, PASSWORD_REGEX } from '../constants';
|
||||
|
||||
export const emailSchema = z.string().regex(EMAIL_REGEX, 'Gecersiz email adresi');
|
||||
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(8, 'Sifre en az 8 karakter olmalidir')
|
||||
.regex(PASSWORD_REGEX, 'Sifre en az bir harf ve bir rakam icermelidir');
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: emailSchema,
|
||||
password: z.string().min(1, 'Sifre gerekli'),
|
||||
});
|
||||
|
||||
export const registerSchema = z.object({
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
name: z.string().min(2, 'Ad en az 2 karakter olmalidir').optional(),
|
||||
});
|
||||
|
||||
export const forgotPasswordSchema = z.object({
|
||||
email: emailSchema,
|
||||
});
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
token: z.string().min(1, 'Token gerekli'),
|
||||
password: passwordSchema,
|
||||
});
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
name: z.string().min(2).max(100).optional(),
|
||||
avatar: z.string().url().optional(),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||
export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;
|
||||
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
||||
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
||||
14
packages/shared/src/schemas/vin.schema.ts
Normal file
14
packages/shared/src/schemas/vin.schema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { VIN_REGEX } from '../constants';
|
||||
|
||||
export const vinSchema = z
|
||||
.string()
|
||||
.length(17, 'VIN 17 karakter olmalidir')
|
||||
.regex(VIN_REGEX, 'Gecersiz VIN formati (I, O, Q kullanilamaz)')
|
||||
.transform((val) => val.toUpperCase());
|
||||
|
||||
export const decodeVinSchema = z.object({
|
||||
vin: vinSchema,
|
||||
});
|
||||
|
||||
export type DecodeVinInput = z.infer<typeof decodeVinSchema>;
|
||||
26
packages/shared/src/types/brand.types.ts
Normal file
26
packages/shared/src/types/brand.types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface Brand {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface BrandListResponse {
|
||||
items: Brand[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SelectBrandsRequest {
|
||||
brandIds: string[];
|
||||
}
|
||||
|
||||
export interface UserBrandResponse {
|
||||
id: string;
|
||||
brandId: string;
|
||||
brand: Brand;
|
||||
createdAt: Date;
|
||||
}
|
||||
51
packages/shared/src/types/category.types.ts
Normal file
51
packages/shared/src/types/category.types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface Category {
|
||||
id: string;
|
||||
code: string;
|
||||
nameEn: string;
|
||||
nameTr: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
parentId: string | null;
|
||||
iconName: string | null;
|
||||
schemaImageUrl: string | null;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CategoryWithChildren extends Category {
|
||||
children: CategoryWithChildren[];
|
||||
}
|
||||
|
||||
export interface CategoryTreeResponse {
|
||||
items: CategoryWithChildren[];
|
||||
}
|
||||
|
||||
export interface CategoryWithParts extends Category {
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
id: string;
|
||||
vehicleId: string;
|
||||
categoryId: string;
|
||||
oemCode: string;
|
||||
oemCodes: string[] | null;
|
||||
nameEn: string;
|
||||
nameTr: string;
|
||||
description: string | null;
|
||||
positionCode: string | null;
|
||||
positionX: number | null;
|
||||
positionY: number | null;
|
||||
brandPrices: BrandPrice[];
|
||||
imageUrl: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface BrandPrice {
|
||||
brand: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
inStock: boolean;
|
||||
}
|
||||
70
packages/shared/src/types/index.ts
Normal file
70
packages/shared/src/types/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// Brand types (source of truth)
|
||||
export * from './brand.types';
|
||||
|
||||
// Subscription types (source of truth for Plan, SubscriptionStatus)
|
||||
export * from './subscription.types';
|
||||
|
||||
// Category types (source of truth for Category, Part, BrandPrice)
|
||||
export * from './category.types';
|
||||
|
||||
// User types (re-exports Brand, Plan, SubscriptionStatus for convenience)
|
||||
export {
|
||||
AuthProvider,
|
||||
User,
|
||||
UserWithSubscription,
|
||||
UserSubscription,
|
||||
UserBrand,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
AuthResponse,
|
||||
JwtPayload,
|
||||
} from './user.types';
|
||||
|
||||
// Vehicle types (re-exports Category, Part, BrandPrice for convenience)
|
||||
export {
|
||||
Vehicle,
|
||||
VehicleWithDetails,
|
||||
VehicleCategory,
|
||||
DecodeVinRequest,
|
||||
DecodeVinResponse,
|
||||
VehicleListResponse,
|
||||
} from './vehicle.types';
|
||||
|
||||
// Payment types
|
||||
export * from './payment.types';
|
||||
|
||||
// Part types
|
||||
export * from './part.types';
|
||||
|
||||
// API Response wrapper
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, string[]>;
|
||||
};
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface PaginationQuery {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
31
packages/shared/src/types/part.types.ts
Normal file
31
packages/shared/src/types/part.types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Part } from './category.types';
|
||||
|
||||
export interface PartListResponse {
|
||||
items: Part[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface PartSearchRequest {
|
||||
oem?: string;
|
||||
brand?: string;
|
||||
vehicleId?: string;
|
||||
categoryId?: string;
|
||||
}
|
||||
|
||||
export interface PartDetailResponse extends Part {
|
||||
vehicle: {
|
||||
id: string;
|
||||
vin: string;
|
||||
brand: string;
|
||||
model: string;
|
||||
year: number;
|
||||
};
|
||||
category: {
|
||||
id: string;
|
||||
nameTr: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
66
packages/shared/src/types/payment.types.ts
Normal file
66
packages/shared/src/types/payment.types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'PENDING',
|
||||
PROCESSING = 'PROCESSING',
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED = 'FAILED',
|
||||
REFUNDED = 'REFUNDED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
id: string;
|
||||
subscriptionId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: PaymentStatus;
|
||||
provider: string;
|
||||
providerTxId: string | null;
|
||||
providerData: Record<string, unknown> | null;
|
||||
invoiceNumber: string | null;
|
||||
invoiceUrl: string | null;
|
||||
failureReason: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface InitializePaymentRequest {
|
||||
planId: string;
|
||||
brandIds: string[];
|
||||
card: {
|
||||
cardHolderName: string;
|
||||
cardNumber: string;
|
||||
expireMonth: string;
|
||||
expireYear: string;
|
||||
cvc: string;
|
||||
};
|
||||
buyer: {
|
||||
name: string;
|
||||
surname: string;
|
||||
phone: string;
|
||||
identityNumber: string;
|
||||
email: string;
|
||||
address: string;
|
||||
city: string;
|
||||
country: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InitializePaymentResponse {
|
||||
status: string;
|
||||
htmlContent?: string;
|
||||
paymentId?: string;
|
||||
conversationId?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface PaymentCallbackRequest {
|
||||
token: string;
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
export interface PaymentHistoryResponse {
|
||||
items: Payment[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
51
packages/shared/src/types/subscription.types.ts
Normal file
51
packages/shared/src/types/subscription.types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface Plan {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
price: number;
|
||||
currency: string;
|
||||
brandLimit: number;
|
||||
hasFullAccess: boolean;
|
||||
features: string[];
|
||||
durationDays: number;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
isPopular: boolean;
|
||||
}
|
||||
|
||||
export interface PlanListResponse {
|
||||
items: Plan[];
|
||||
}
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
PAST_DUE = 'PAST_DUE',
|
||||
CANCELLED = 'CANCELLED',
|
||||
EXPIRED = 'EXPIRED',
|
||||
SUSPENDED = 'SUSPENDED',
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
userId: string;
|
||||
planId: string;
|
||||
status: SubscriptionStatus;
|
||||
currentPeriodStart: Date;
|
||||
currentPeriodEnd: Date;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
cancelledAt: Date | null;
|
||||
plan: Plan;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateSubscriptionRequest {
|
||||
planId: string;
|
||||
brandIds: string[];
|
||||
}
|
||||
|
||||
export interface SubscriptionResponse {
|
||||
subscription: Subscription;
|
||||
selectedBrands: { id: string; code: string; name: string }[];
|
||||
}
|
||||
72
packages/shared/src/types/user.types.ts
Normal file
72
packages/shared/src/types/user.types.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Brand } from './brand.types';
|
||||
import { Plan, SubscriptionStatus } from './subscription.types';
|
||||
|
||||
export { Brand, Plan, SubscriptionStatus };
|
||||
|
||||
export enum AuthProvider {
|
||||
EMAIL = 'EMAIL',
|
||||
GOOGLE = 'GOOGLE',
|
||||
APPLE = 'APPLE',
|
||||
FACEBOOK = 'FACEBOOK',
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
avatar: string | null;
|
||||
provider: AuthProvider;
|
||||
providerId: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface UserWithSubscription extends User {
|
||||
subscription: UserSubscription | null;
|
||||
selectedBrands: UserBrand[];
|
||||
}
|
||||
|
||||
export interface UserSubscription {
|
||||
id: string;
|
||||
userId: string;
|
||||
planId: string;
|
||||
status: SubscriptionStatus;
|
||||
currentPeriodStart: Date;
|
||||
currentPeriodEnd: Date;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
cancelledAt: Date | null;
|
||||
plan: Plan;
|
||||
}
|
||||
|
||||
export interface UserBrand {
|
||||
id: string;
|
||||
userId: string;
|
||||
brandId: string;
|
||||
brand: Brand;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
56
packages/shared/src/types/vehicle.types.ts
Normal file
56
packages/shared/src/types/vehicle.types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Brand } from './brand.types';
|
||||
import { Category, Part, BrandPrice } from './category.types';
|
||||
|
||||
export { Category, Part, BrandPrice };
|
||||
|
||||
export interface Vehicle {
|
||||
id: string;
|
||||
vin: string;
|
||||
brandId: string;
|
||||
brand: Brand;
|
||||
model: string;
|
||||
year: number;
|
||||
series: string | null;
|
||||
bodyType: string | null;
|
||||
engineCode: string | null;
|
||||
engineType: string | null;
|
||||
engineVolume: string | null;
|
||||
transmission: string | null;
|
||||
driveType: string | null;
|
||||
colorCode: string | null;
|
||||
rawResponse: Record<string, unknown>;
|
||||
queriedById: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface VehicleWithDetails extends Vehicle {
|
||||
categories: VehicleCategory[];
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
export interface VehicleCategory {
|
||||
id: string;
|
||||
vehicleId: string;
|
||||
categoryId: string;
|
||||
partCount: number;
|
||||
category: Category;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface DecodeVinRequest {
|
||||
vin: string;
|
||||
}
|
||||
|
||||
export interface DecodeVinResponse {
|
||||
vehicle: VehicleWithDetails;
|
||||
fromCache: boolean;
|
||||
}
|
||||
|
||||
export interface VehicleListResponse {
|
||||
items: Vehicle[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
}
|
||||
50
packages/shared/src/utils/currency.ts
Normal file
50
packages/shared/src/utils/currency.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export const CURRENCIES = {
|
||||
TRY: { symbol: '₺', name: 'Turk Lirasi', locale: 'tr-TR' },
|
||||
USD: { symbol: '$', name: 'ABD Dolari', locale: 'en-US' },
|
||||
EUR: { symbol: '€', name: 'Euro', locale: 'de-DE' },
|
||||
} as const;
|
||||
|
||||
export type CurrencyCode = keyof typeof CURRENCIES;
|
||||
|
||||
export function getCurrencySymbol(code: CurrencyCode): string {
|
||||
return CURRENCIES[code]?.symbol || code;
|
||||
}
|
||||
|
||||
export function formatPrice(amount: number, currency: CurrencyCode = 'TRY'): string {
|
||||
const config = CURRENCIES[currency];
|
||||
|
||||
return new Intl.NumberFormat(config?.locale || 'tr-TR', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export function parsePriceString(priceString: string): number {
|
||||
// Remove currency symbols and whitespace
|
||||
const cleaned = priceString.replace(/[^\d.,]/g, '');
|
||||
|
||||
// Handle Turkish format (1.234,56) vs US format (1,234.56)
|
||||
if (cleaned.includes(',') && cleaned.includes('.')) {
|
||||
// Turkish format: dots for thousands, comma for decimal
|
||||
if (cleaned.lastIndexOf(',') > cleaned.lastIndexOf('.')) {
|
||||
return parseFloat(cleaned.replace(/\./g, '').replace(',', '.'));
|
||||
}
|
||||
// US format
|
||||
return parseFloat(cleaned.replace(/,/g, ''));
|
||||
}
|
||||
|
||||
// Only comma: could be Turkish decimal or US thousands
|
||||
if (cleaned.includes(',')) {
|
||||
const parts = cleaned.split(',');
|
||||
if (parts[parts.length - 1].length === 2) {
|
||||
// Likely decimal
|
||||
return parseFloat(cleaned.replace(',', '.'));
|
||||
}
|
||||
// Likely thousands separator
|
||||
return parseFloat(cleaned.replace(/,/g, ''));
|
||||
}
|
||||
|
||||
return parseFloat(cleaned);
|
||||
}
|
||||
78
packages/shared/src/utils/formatters.ts
Normal file
78
packages/shared/src/utils/formatters.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { DEFAULT_CURRENCY } from '../constants';
|
||||
|
||||
export function formatCurrency(amount: number, currency: string = DEFAULT_CURRENCY): string {
|
||||
const formatter = new Intl.NumberFormat('tr-TR', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
export function formatDate(date: Date | string, options?: Intl.DateTimeFormatOptions): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
|
||||
return d.toLocaleDateString('tr-TR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date | string): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
|
||||
return d.toLocaleString('tr-TR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export function formatVin(vin: string): string {
|
||||
// Format: XXX XXXXXX XXXXXXXX (WMI VDS VIS)
|
||||
const normalized = vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/gi, '');
|
||||
if (normalized.length !== 17) return vin;
|
||||
|
||||
return `${normalized.slice(0, 3)} ${normalized.slice(3, 9)} ${normalized.slice(9)}`;
|
||||
}
|
||||
|
||||
export function slugify(text: string): string {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
}
|
||||
|
||||
export function truncate(text: string, length: number, suffix: string = '...'): string {
|
||||
if (text.length <= length) return text;
|
||||
return text.slice(0, length - suffix.length) + suffix;
|
||||
}
|
||||
|
||||
export function capitalizeFirst(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
export function formatPhoneNumber(phone: string): string {
|
||||
const cleaned = phone.replace(/\D/g, '');
|
||||
|
||||
if (cleaned.length === 10) {
|
||||
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)} ${cleaned.slice(6, 8)} ${cleaned.slice(8)}`;
|
||||
}
|
||||
|
||||
if (cleaned.length === 11 && cleaned.startsWith('0')) {
|
||||
return `(${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)} ${cleaned.slice(7, 9)} ${cleaned.slice(9)}`;
|
||||
}
|
||||
|
||||
return phone;
|
||||
}
|
||||
60
packages/shared/src/utils/index.ts
Normal file
60
packages/shared/src/utils/index.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export * from './vin-validator';
|
||||
export * from './formatters';
|
||||
export * from './currency';
|
||||
|
||||
// General utilities
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function isEmptyObject(obj: object): boolean {
|
||||
return Object.keys(obj).length === 0;
|
||||
}
|
||||
|
||||
export function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
||||
const result = { ...obj };
|
||||
keys.forEach((key) => delete result[key]);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||
const result = {} as Pick<T, K>;
|
||||
keys.forEach((key) => {
|
||||
if (key in obj) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
|
||||
return array.reduce(
|
||||
(result, item) => {
|
||||
const groupKey = String(item[key]);
|
||||
if (!result[groupKey]) {
|
||||
result[groupKey] = [];
|
||||
}
|
||||
result[groupKey].push(item);
|
||||
return result;
|
||||
},
|
||||
{} as Record<string, T[]>,
|
||||
);
|
||||
}
|
||||
|
||||
export function uniqueBy<T>(array: T[], key: keyof T): T[] {
|
||||
const seen = new Set<unknown>();
|
||||
return array.filter((item) => {
|
||||
const value = item[key];
|
||||
if (seen.has(value)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function generateId(prefix: string = ''): string {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 9);
|
||||
return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`;
|
||||
}
|
||||
175
packages/shared/src/utils/vin-validator.ts
Normal file
175
packages/shared/src/utils/vin-validator.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { VIN_REGEX, VIN_LENGTH } from '../constants';
|
||||
|
||||
const TRANSLITERATION: Record<string, number> = {
|
||||
A: 1,
|
||||
B: 2,
|
||||
C: 3,
|
||||
D: 4,
|
||||
E: 5,
|
||||
F: 6,
|
||||
G: 7,
|
||||
H: 8,
|
||||
J: 1,
|
||||
K: 2,
|
||||
L: 3,
|
||||
M: 4,
|
||||
N: 5,
|
||||
P: 7,
|
||||
R: 9,
|
||||
S: 2,
|
||||
T: 3,
|
||||
U: 4,
|
||||
V: 5,
|
||||
W: 6,
|
||||
X: 7,
|
||||
Y: 8,
|
||||
Z: 9,
|
||||
};
|
||||
|
||||
const WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
|
||||
|
||||
export function validateVin(vin: string): { valid: boolean; error?: string } {
|
||||
if (!vin) {
|
||||
return { valid: false, error: 'VIN numarasi gerekli' };
|
||||
}
|
||||
|
||||
const normalizedVin = vin.toUpperCase().trim();
|
||||
|
||||
if (normalizedVin.length !== VIN_LENGTH) {
|
||||
return { valid: false, error: `VIN ${VIN_LENGTH} karakter olmalidir` };
|
||||
}
|
||||
|
||||
if (!VIN_REGEX.test(normalizedVin)) {
|
||||
return { valid: false, error: 'VIN gecersiz karakterler iceriyor (I, O, Q kullanilamaz)' };
|
||||
}
|
||||
|
||||
// Check digit validation (position 9)
|
||||
const checkDigit = calculateCheckDigit(normalizedVin);
|
||||
const actualCheckDigit = normalizedVin[8];
|
||||
|
||||
if (checkDigit !== actualCheckDigit) {
|
||||
// Some manufacturers don't follow check digit standard
|
||||
// So we just warn but don't fail
|
||||
console.warn(`VIN check digit mismatch: expected ${checkDigit}, got ${actualCheckDigit}`);
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function calculateCheckDigit(vin: string): string {
|
||||
let sum = 0;
|
||||
|
||||
for (let i = 0; i < 17; i++) {
|
||||
const char = vin[i];
|
||||
let value: number;
|
||||
|
||||
if (/[0-9]/.test(char)) {
|
||||
value = parseInt(char, 10);
|
||||
} else {
|
||||
value = TRANSLITERATION[char] || 0;
|
||||
}
|
||||
|
||||
sum += value * WEIGHTS[i];
|
||||
}
|
||||
|
||||
const remainder = sum % 11;
|
||||
return remainder === 10 ? 'X' : remainder.toString();
|
||||
}
|
||||
|
||||
export function normalizeVin(vin: string): string {
|
||||
return vin.toUpperCase().trim().replace(/[^A-HJ-NPR-Z0-9]/gi, '');
|
||||
}
|
||||
|
||||
export function getVinInfo(vin: string): {
|
||||
wmi: string;
|
||||
vds: string;
|
||||
vis: string;
|
||||
region: string;
|
||||
year: number | null;
|
||||
} {
|
||||
const normalizedVin = normalizeVin(vin);
|
||||
|
||||
return {
|
||||
wmi: normalizedVin.substring(0, 3), // World Manufacturer Identifier
|
||||
vds: normalizedVin.substring(3, 9), // Vehicle Descriptor Section
|
||||
vis: normalizedVin.substring(9, 17), // Vehicle Identifier Section
|
||||
region: getRegion(normalizedVin[0]),
|
||||
year: getModelYear(normalizedVin[9]),
|
||||
};
|
||||
}
|
||||
|
||||
function getRegion(char: string): string {
|
||||
const regions: Record<string, string> = {
|
||||
A: 'Africa',
|
||||
B: 'Africa',
|
||||
C: 'Africa',
|
||||
D: 'Africa',
|
||||
E: 'Africa',
|
||||
F: 'Africa',
|
||||
G: 'Africa',
|
||||
H: 'Africa',
|
||||
J: 'Asia',
|
||||
K: 'Asia',
|
||||
L: 'Asia',
|
||||
M: 'Asia',
|
||||
N: 'Asia',
|
||||
P: 'Asia',
|
||||
R: 'Asia',
|
||||
S: 'Europe',
|
||||
T: 'Europe',
|
||||
U: 'Europe',
|
||||
V: 'Europe',
|
||||
W: 'Europe',
|
||||
X: 'Europe',
|
||||
Y: 'Europe',
|
||||
Z: 'Europe',
|
||||
'1': 'North America',
|
||||
'2': 'North America',
|
||||
'3': 'North America',
|
||||
'4': 'North America',
|
||||
'5': 'North America',
|
||||
'6': 'Oceania',
|
||||
'7': 'Oceania',
|
||||
'8': 'South America',
|
||||
'9': 'South America',
|
||||
};
|
||||
|
||||
return regions[char] || 'Unknown';
|
||||
}
|
||||
|
||||
function getModelYear(char: string): number | null {
|
||||
const years: Record<string, number> = {
|
||||
A: 2010,
|
||||
B: 2011,
|
||||
C: 2012,
|
||||
D: 2013,
|
||||
E: 2014,
|
||||
F: 2015,
|
||||
G: 2016,
|
||||
H: 2017,
|
||||
J: 2018,
|
||||
K: 2019,
|
||||
L: 2020,
|
||||
M: 2021,
|
||||
N: 2022,
|
||||
P: 2023,
|
||||
R: 2024,
|
||||
S: 2025,
|
||||
T: 2026,
|
||||
V: 2027,
|
||||
W: 2028,
|
||||
X: 2029,
|
||||
Y: 2030,
|
||||
'1': 2031,
|
||||
'2': 2032,
|
||||
'3': 2033,
|
||||
'4': 2034,
|
||||
'5': 2035,
|
||||
'6': 2036,
|
||||
'7': 2037,
|
||||
'8': 2038,
|
||||
'9': 2039,
|
||||
};
|
||||
|
||||
return years[char] || null;
|
||||
}
|
||||
20
packages/shared/tsconfig.json
Normal file
20
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
1
packages/shared/tsconfig.tsbuildinfo
Normal file
1
packages/shared/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user