@compilr-dev/factory 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/factory/registry.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/toolkits/next-prisma/api-routes.d.ts +10 -0
- package/dist/toolkits/next-prisma/api-routes.js +155 -0
- package/dist/toolkits/next-prisma/config.d.ts +9 -0
- package/dist/toolkits/next-prisma/config.js +139 -0
- package/dist/toolkits/next-prisma/dashboard.d.ts +9 -0
- package/dist/toolkits/next-prisma/dashboard.js +87 -0
- package/dist/toolkits/next-prisma/entity-components.d.ts +10 -0
- package/dist/toolkits/next-prisma/entity-components.js +217 -0
- package/dist/toolkits/next-prisma/entity-pages.d.ts +12 -0
- package/dist/toolkits/next-prisma/entity-pages.js +348 -0
- package/dist/toolkits/next-prisma/helpers.d.ts +13 -0
- package/dist/toolkits/next-prisma/helpers.js +37 -0
- package/dist/toolkits/next-prisma/index.d.ts +9 -0
- package/dist/toolkits/next-prisma/index.js +57 -0
- package/dist/toolkits/next-prisma/layout.d.ts +9 -0
- package/dist/toolkits/next-prisma/layout.js +157 -0
- package/dist/toolkits/next-prisma/prisma.d.ts +8 -0
- package/dist/toolkits/next-prisma/prisma.js +76 -0
- package/dist/toolkits/next-prisma/seed.d.ts +9 -0
- package/dist/toolkits/next-prisma/seed.js +100 -0
- package/dist/toolkits/next-prisma/static.d.ts +8 -0
- package/dist/toolkits/next-prisma/static.js +61 -0
- package/dist/toolkits/next-prisma/types-gen.d.ts +10 -0
- package/dist/toolkits/next-prisma/types-gen.js +62 -0
- package/dist/toolkits/react-node/api.js +96 -13
- package/dist/toolkits/react-node/config.d.ts +1 -4
- package/dist/toolkits/react-node/config.js +3 -84
- package/dist/toolkits/react-node/entity.js +64 -22
- package/dist/toolkits/react-node/helpers.d.ts +2 -23
- package/dist/toolkits/react-node/helpers.js +2 -67
- package/dist/toolkits/react-node/seed.d.ts +4 -3
- package/dist/toolkits/react-node/seed.js +4 -111
- package/dist/toolkits/react-node/shared.d.ts +2 -3
- package/dist/toolkits/react-node/shared.js +2 -115
- package/dist/toolkits/react-node/types-gen.js +26 -0
- package/dist/toolkits/shared/color-utils.d.ts +10 -0
- package/dist/toolkits/shared/color-utils.js +85 -0
- package/dist/toolkits/shared/components.d.ts +12 -0
- package/dist/toolkits/shared/components.js +121 -0
- package/dist/toolkits/shared/helpers.d.ts +28 -0
- package/dist/toolkits/shared/helpers.js +72 -0
- package/dist/toolkits/shared/index.d.ts +9 -0
- package/dist/toolkits/shared/index.js +9 -0
- package/dist/toolkits/shared/seed-data.d.ts +18 -0
- package/dist/toolkits/shared/seed-data.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared React Components Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: ViewToggle.tsx, SearchBar.tsx, FilterBar.tsx
|
|
5
|
+
* These are framework-agnostic React components reusable across toolkits.
|
|
6
|
+
*/
|
|
7
|
+
export function generateViewToggle() {
|
|
8
|
+
return {
|
|
9
|
+
path: 'src/components/shared/ViewToggle.tsx',
|
|
10
|
+
content: `interface ViewToggleProps {
|
|
11
|
+
view: 'card' | 'list';
|
|
12
|
+
onChange: (view: 'card' | 'list') => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function ViewToggle({ view, onChange }: ViewToggleProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex rounded-lg border border-gray-300 dark:border-gray-600">
|
|
18
|
+
<button
|
|
19
|
+
onClick={() => onChange('card')}
|
|
20
|
+
className={\`px-3 py-1 text-sm \${view === 'card' ? 'bg-primary text-white' : 'text-gray-600 dark:text-gray-300'}\`}
|
|
21
|
+
>
|
|
22
|
+
Grid
|
|
23
|
+
</button>
|
|
24
|
+
<button
|
|
25
|
+
onClick={() => onChange('list')}
|
|
26
|
+
className={\`px-3 py-1 text-sm \${view === 'list' ? 'bg-primary text-white' : 'text-gray-600 dark:text-gray-300'}\`}
|
|
27
|
+
>
|
|
28
|
+
List
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function generateSearchBar() {
|
|
37
|
+
return {
|
|
38
|
+
path: 'src/components/shared/SearchBar.tsx',
|
|
39
|
+
content: `import { useState, useEffect } from 'react';
|
|
40
|
+
|
|
41
|
+
interface SearchBarProps {
|
|
42
|
+
onSearch: (query: string) => void;
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function SearchBar({ onSearch, placeholder = 'Search...' }: SearchBarProps) {
|
|
47
|
+
const [value, setValue] = useState('');
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const timer = setTimeout(() => onSearch(value), 300);
|
|
51
|
+
return () => clearTimeout(timer);
|
|
52
|
+
}, [value, onSearch]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="relative">
|
|
56
|
+
<input
|
|
57
|
+
type="text"
|
|
58
|
+
value={value}
|
|
59
|
+
onChange={(e) => setValue(e.target.value)}
|
|
60
|
+
placeholder={placeholder}
|
|
61
|
+
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-primary focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
62
|
+
/>
|
|
63
|
+
{value && (
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => setValue('')}
|
|
66
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
67
|
+
>
|
|
68
|
+
×
|
|
69
|
+
</button>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function generateFilterBar() {
|
|
78
|
+
return {
|
|
79
|
+
path: 'src/components/shared/FilterBar.tsx',
|
|
80
|
+
content: `interface FilterOption {
|
|
81
|
+
label: string;
|
|
82
|
+
field: string;
|
|
83
|
+
values: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface FilterBarProps {
|
|
87
|
+
filters: FilterOption[];
|
|
88
|
+
active: Record<string, string>;
|
|
89
|
+
onChange: (field: string, value: string) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default function FilterBar({ filters, active, onChange }: FilterBarProps) {
|
|
93
|
+
if (filters.length === 0) return null;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex flex-wrap gap-2">
|
|
97
|
+
{filters.map((filter) => (
|
|
98
|
+
<select
|
|
99
|
+
key={filter.field}
|
|
100
|
+
value={active[filter.field] ?? ''}
|
|
101
|
+
onChange={(e) => onChange(filter.field, e.target.value)}
|
|
102
|
+
className="rounded-lg border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
103
|
+
>
|
|
104
|
+
<option value="">{filter.label}: All</option>
|
|
105
|
+
{filter.values.map((v) => (
|
|
106
|
+
<option key={v} value={v}>
|
|
107
|
+
{v}
|
|
108
|
+
</option>
|
|
109
|
+
))}
|
|
110
|
+
</select>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Generate all shared components (ViewToggle, SearchBar, FilterBar). */
|
|
119
|
+
export function generateSharedComponents() {
|
|
120
|
+
return [generateViewToggle(), generateSearchBar(), generateFilterBar()];
|
|
121
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Toolkit Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utility functions used across toolkit generators.
|
|
5
|
+
* These operate on model types (Entity, Field, Relationship) — no framework coupling.
|
|
6
|
+
*/
|
|
7
|
+
import type { Entity, Field, Relationship } from '../../model/types.js';
|
|
8
|
+
/** Indent every line of a multi-line string. */
|
|
9
|
+
export declare function indent(text: string, spaces: number): string;
|
|
10
|
+
/** Map a model FieldType to a TypeScript type string. */
|
|
11
|
+
export declare function tsType(field: Field): string;
|
|
12
|
+
/** Get the FK field name for a belongsTo relationship. */
|
|
13
|
+
export declare function fkFieldName(rel: Relationship): string;
|
|
14
|
+
/** Get belongsTo relationships for an entity. */
|
|
15
|
+
export declare function belongsToRels(entity: Entity): readonly Relationship[];
|
|
16
|
+
/** Get hasMany relationships for an entity. */
|
|
17
|
+
export declare function hasManyRels(entity: Entity): readonly Relationship[];
|
|
18
|
+
/** Generate a route path from entity plural name. */
|
|
19
|
+
export declare function routePath(entity: Entity): string;
|
|
20
|
+
/** Generate an API path from entity plural name. */
|
|
21
|
+
export declare function apiPath(entity: Entity): string;
|
|
22
|
+
/** Map a model FieldType to an HTML input type. */
|
|
23
|
+
export declare function inputType(field: Field): string;
|
|
24
|
+
/** Generate import statement lines. */
|
|
25
|
+
export declare function generateImports(imports: Array<{
|
|
26
|
+
from: string;
|
|
27
|
+
names: string[];
|
|
28
|
+
}>): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Toolkit Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utility functions used across toolkit generators.
|
|
5
|
+
* These operate on model types (Entity, Field, Relationship) — no framework coupling.
|
|
6
|
+
*/
|
|
7
|
+
import { toCamelCase, toKebabCase } from '../../model/naming.js';
|
|
8
|
+
/** Indent every line of a multi-line string. */
|
|
9
|
+
export function indent(text, spaces) {
|
|
10
|
+
const pad = ' '.repeat(spaces);
|
|
11
|
+
return text
|
|
12
|
+
.split('\n')
|
|
13
|
+
.map((line) => (line.trim() === '' ? '' : pad + line))
|
|
14
|
+
.join('\n');
|
|
15
|
+
}
|
|
16
|
+
/** Map a model FieldType to a TypeScript type string. */
|
|
17
|
+
export function tsType(field) {
|
|
18
|
+
switch (field.type) {
|
|
19
|
+
case 'string':
|
|
20
|
+
case 'enum':
|
|
21
|
+
return 'string';
|
|
22
|
+
case 'number':
|
|
23
|
+
return 'number';
|
|
24
|
+
case 'boolean':
|
|
25
|
+
return 'boolean';
|
|
26
|
+
case 'date':
|
|
27
|
+
return 'string'; // ISO date string
|
|
28
|
+
default:
|
|
29
|
+
return 'string';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Get the FK field name for a belongsTo relationship. */
|
|
33
|
+
export function fkFieldName(rel) {
|
|
34
|
+
return rel.fieldName ?? toCamelCase(rel.target) + 'Id';
|
|
35
|
+
}
|
|
36
|
+
/** Get belongsTo relationships for an entity. */
|
|
37
|
+
export function belongsToRels(entity) {
|
|
38
|
+
return entity.relationships.filter((r) => r.type === 'belongsTo');
|
|
39
|
+
}
|
|
40
|
+
/** Get hasMany relationships for an entity. */
|
|
41
|
+
export function hasManyRels(entity) {
|
|
42
|
+
return entity.relationships.filter((r) => r.type === 'hasMany');
|
|
43
|
+
}
|
|
44
|
+
/** Generate a route path from entity plural name. */
|
|
45
|
+
export function routePath(entity) {
|
|
46
|
+
return '/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
/** Generate an API path from entity plural name. */
|
|
49
|
+
export function apiPath(entity) {
|
|
50
|
+
return '/api/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
/** Map a model FieldType to an HTML input type. */
|
|
53
|
+
export function inputType(field) {
|
|
54
|
+
switch (field.type) {
|
|
55
|
+
case 'string':
|
|
56
|
+
return 'text';
|
|
57
|
+
case 'number':
|
|
58
|
+
return 'number';
|
|
59
|
+
case 'boolean':
|
|
60
|
+
return 'checkbox';
|
|
61
|
+
case 'date':
|
|
62
|
+
return 'date';
|
|
63
|
+
case 'enum':
|
|
64
|
+
return 'select';
|
|
65
|
+
default:
|
|
66
|
+
return 'text';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Generate import statement lines. */
|
|
70
|
+
export function generateImports(imports) {
|
|
71
|
+
return imports.map((i) => `import { ${i.names.join(', ')} } from '${i.from}';`).join('\n');
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Toolkit Utilities
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from all shared modules for convenience.
|
|
5
|
+
*/
|
|
6
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from './helpers.js';
|
|
7
|
+
export { hexToHsl, hslToHex, generateColorShades, SHADE_LIGHTNESS } from './color-utils.js';
|
|
8
|
+
export { generateViewToggle, generateSearchBar, generateFilterBar, generateSharedComponents, } from './components.js';
|
|
9
|
+
export { generateFieldValue, SEED_COUNT, PERSON_NAMES, COMPANY_NAMES, PRODUCT_NAMES, DESCRIPTIONS, EMAILS, } from './seed-data.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Toolkit Utilities
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from all shared modules for convenience.
|
|
5
|
+
*/
|
|
6
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from './helpers.js';
|
|
7
|
+
export { hexToHsl, hslToHex, generateColorShades, SHADE_LIGHTNESS } from './color-utils.js';
|
|
8
|
+
export { generateViewToggle, generateSearchBar, generateFilterBar, generateSharedComponents, } from './components.js';
|
|
9
|
+
export { generateFieldValue, SEED_COUNT, PERSON_NAMES, COMPANY_NAMES, PRODUCT_NAMES, DESCRIPTIONS, EMAILS, } from './seed-data.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Seed Data Utilities
|
|
3
|
+
*
|
|
4
|
+
* Deterministic mock data generation based on field semantics.
|
|
5
|
+
* Same input → same output (uses index-based logic, not Math.random).
|
|
6
|
+
*
|
|
7
|
+
* Toolkit-specific wrappers (e.g., JS array format, Prisma createMany) import these
|
|
8
|
+
* and format the output for their target framework.
|
|
9
|
+
*/
|
|
10
|
+
import type { Field } from '../../model/types.js';
|
|
11
|
+
export declare const PERSON_NAMES: string[];
|
|
12
|
+
export declare const COMPANY_NAMES: string[];
|
|
13
|
+
export declare const PRODUCT_NAMES: string[];
|
|
14
|
+
export declare const DESCRIPTIONS: string[];
|
|
15
|
+
export declare const EMAILS: string[];
|
|
16
|
+
export declare const SEED_COUNT = 8;
|
|
17
|
+
/** Generate a deterministic string value for a single field at a given index. */
|
|
18
|
+
export declare function generateFieldValue(field: Field, index: number, entityName: string): string;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Seed Data Utilities
|
|
3
|
+
*
|
|
4
|
+
* Deterministic mock data generation based on field semantics.
|
|
5
|
+
* Same input → same output (uses index-based logic, not Math.random).
|
|
6
|
+
*
|
|
7
|
+
* Toolkit-specific wrappers (e.g., JS array format, Prisma createMany) import these
|
|
8
|
+
* and format the output for their target framework.
|
|
9
|
+
*/
|
|
10
|
+
export const PERSON_NAMES = [
|
|
11
|
+
'Alice Johnson',
|
|
12
|
+
'Bob Smith',
|
|
13
|
+
'Carol Davis',
|
|
14
|
+
'David Lee',
|
|
15
|
+
'Eva Martinez',
|
|
16
|
+
'Frank Wilson',
|
|
17
|
+
'Grace Chen',
|
|
18
|
+
'Henry Brown',
|
|
19
|
+
'Iris Kim',
|
|
20
|
+
'Jack Taylor',
|
|
21
|
+
];
|
|
22
|
+
export const COMPANY_NAMES = [
|
|
23
|
+
'Acme Corp',
|
|
24
|
+
'TechVista Inc',
|
|
25
|
+
'BlueSky Solutions',
|
|
26
|
+
'Greenfield LLC',
|
|
27
|
+
'Summit Dynamics',
|
|
28
|
+
'Cascade Systems',
|
|
29
|
+
'Meridian Labs',
|
|
30
|
+
'Atlas Ventures',
|
|
31
|
+
];
|
|
32
|
+
export const PRODUCT_NAMES = [
|
|
33
|
+
'Premium Widget',
|
|
34
|
+
'Standard Package',
|
|
35
|
+
'Enterprise Suite',
|
|
36
|
+
'Starter Kit',
|
|
37
|
+
'Pro Bundle',
|
|
38
|
+
'Deluxe Edition',
|
|
39
|
+
'Basic Plan',
|
|
40
|
+
'Advanced Toolkit',
|
|
41
|
+
];
|
|
42
|
+
export const DESCRIPTIONS = [
|
|
43
|
+
'A comprehensive solution for modern needs.',
|
|
44
|
+
'Designed for efficiency and reliability.',
|
|
45
|
+
'Built with quality and care.',
|
|
46
|
+
'Perfect for teams of any size.',
|
|
47
|
+
'Streamlined for maximum productivity.',
|
|
48
|
+
'Industry-leading performance.',
|
|
49
|
+
'Trusted by thousands of users.',
|
|
50
|
+
'Next-generation technology.',
|
|
51
|
+
];
|
|
52
|
+
export const EMAILS = [
|
|
53
|
+
'alice@example.com',
|
|
54
|
+
'bob@example.com',
|
|
55
|
+
'carol@example.com',
|
|
56
|
+
'david@example.com',
|
|
57
|
+
'eva@example.com',
|
|
58
|
+
'frank@example.com',
|
|
59
|
+
'grace@example.com',
|
|
60
|
+
'henry@example.com',
|
|
61
|
+
];
|
|
62
|
+
export const SEED_COUNT = 8;
|
|
63
|
+
/** Generate a deterministic string value for a single field at a given index. */
|
|
64
|
+
export function generateFieldValue(field, index, entityName) {
|
|
65
|
+
const lowerName = field.name.toLowerCase();
|
|
66
|
+
const lowerEntity = entityName.toLowerCase();
|
|
67
|
+
switch (field.type) {
|
|
68
|
+
case 'string':
|
|
69
|
+
if (lowerName.includes('email'))
|
|
70
|
+
return `'${EMAILS[index % EMAILS.length]}'`;
|
|
71
|
+
if (lowerName.includes('name') || lowerName === 'title') {
|
|
72
|
+
if (lowerEntity.includes('customer') ||
|
|
73
|
+
lowerEntity.includes('user') ||
|
|
74
|
+
lowerEntity.includes('person') ||
|
|
75
|
+
lowerEntity.includes('employee')) {
|
|
76
|
+
return `'${PERSON_NAMES[index % PERSON_NAMES.length]}'`;
|
|
77
|
+
}
|
|
78
|
+
if (lowerEntity.includes('company') || lowerEntity.includes('organization')) {
|
|
79
|
+
return `'${COMPANY_NAMES[index % COMPANY_NAMES.length]}'`;
|
|
80
|
+
}
|
|
81
|
+
return `'${PRODUCT_NAMES[index % PRODUCT_NAMES.length]}'`;
|
|
82
|
+
}
|
|
83
|
+
if (lowerName.includes('description') ||
|
|
84
|
+
lowerName.includes('note') ||
|
|
85
|
+
lowerName.includes('comment')) {
|
|
86
|
+
return `'${DESCRIPTIONS[index % DESCRIPTIONS.length]}'`;
|
|
87
|
+
}
|
|
88
|
+
if (lowerName.includes('phone'))
|
|
89
|
+
return `'555-010${String(index)}'`;
|
|
90
|
+
if (lowerName.includes('address'))
|
|
91
|
+
return `'${String(100 + index * 10)} Main St'`;
|
|
92
|
+
return `'${field.label} ${String(index + 1)}'`;
|
|
93
|
+
case 'number':
|
|
94
|
+
if (lowerName.includes('price') || lowerName.includes('cost') || lowerName.includes('amount'))
|
|
95
|
+
return String(10 + index * 15 + 0.99);
|
|
96
|
+
if (lowerName.includes('quantity') || lowerName.includes('count'))
|
|
97
|
+
return String(1 + (index % 10));
|
|
98
|
+
if (lowerName.includes('age'))
|
|
99
|
+
return String(22 + index * 5);
|
|
100
|
+
if (lowerName.includes('size') || lowerName.includes('capacity'))
|
|
101
|
+
return String(2 + (index % 8));
|
|
102
|
+
return String(index + 1);
|
|
103
|
+
case 'boolean':
|
|
104
|
+
return index % 2 === 0 ? 'true' : 'false';
|
|
105
|
+
case 'date': {
|
|
106
|
+
const base = new Date('2025-06-15');
|
|
107
|
+
base.setDate(base.getDate() + (index * 3 - 10));
|
|
108
|
+
return `'${base.toISOString().split('T')[0]}'`;
|
|
109
|
+
}
|
|
110
|
+
case 'enum': {
|
|
111
|
+
const values = field.enumValues ?? [];
|
|
112
|
+
if (values.length === 0)
|
|
113
|
+
return "''";
|
|
114
|
+
return `'${values[index % values.length]}'`;
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
return `'${field.label} ${String(index + 1)}'`;
|
|
118
|
+
}
|
|
119
|
+
}
|