5htp-core 0.2.4 → 0.2.5-2
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/package.json +5 -2
- package/src/client/assets/css/text/text.less +0 -26
- package/src/client/components/Amount.tsx +1 -1
- package/src/client/pages/_layout/index.less +6 -0
- package/src/client/pages/_layout/index.tsx +43 -0
- package/src/client/pages/_messages/400.tsx +8 -9
- package/src/client/pages/_messages/401.tsx +20 -3
- package/src/client/pages/_messages/403.tsx +6 -6
- package/src/client/pages/_messages/404.tsx +7 -7
- package/src/client/pages/_messages/500.tsx +6 -6
- package/src/client/services/router/index.tsx +5 -1
- package/src/common/errors/index.ts +1 -1
- package/src/common/router/layouts.ts +22 -23
- package/src/common/validation/index.ts +2 -1
- package/src/server/app/index.ts +22 -3
- package/src/server/app/service.ts +3 -0
- package/src/server/services/cache/index.ts +0 -0
- package/src/server/services/console/index.ts +111 -28
- package/src/server/services/database/connection.ts +49 -32
- package/src/server/services/router/index.ts +13 -5
- package/src/client/pages/_layout/base.less +0 -13
- package/src/client/pages/_layout/landing/index.less +0 -5
- package/src/client/pages/_layout/landing/index.tsx +0 -55
- package/src/client/pages/_messages/403.svg +0 -1835
- package/src/client/pages/_messages/404.svg +0 -205
- package/src/client/pages/_messages/500.svg +0 -396
- package/src/server/services/console/bugReporter.ts +0 -250
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*----------------------------------
|
|
2
2
|
- DEPENDANCES
|
|
3
3
|
----------------------------------*/
|
|
4
|
-
|
|
5
4
|
// Npm
|
|
5
|
+
import { v4 as uuid } from 'uuid';
|
|
6
6
|
import { Logger, ILogObject } from "tslog";
|
|
7
7
|
import { format as formatSql } from 'sql-formatter';
|
|
8
8
|
import highlight from 'cli-highlight';
|
|
@@ -11,10 +11,10 @@ import highlight from 'cli-highlight';
|
|
|
11
11
|
import Application, { Service, TPriority } from '@server/app';
|
|
12
12
|
import context from '@server/context';
|
|
13
13
|
import type ServerRequest from '@server/services/router/request';
|
|
14
|
+
import { SqlError } from '@server/services/database/debug';
|
|
14
15
|
|
|
15
16
|
// Specific
|
|
16
17
|
import logToHTML from './html';
|
|
17
|
-
import BugReporter from "./bugReporter";
|
|
18
18
|
|
|
19
19
|
/*----------------------------------
|
|
20
20
|
- SERVICE CONFIG
|
|
@@ -23,6 +23,8 @@ import BugReporter from "./bugReporter";
|
|
|
23
23
|
type TLogProfile = 'silly' | 'info' | 'warn' | 'error'
|
|
24
24
|
|
|
25
25
|
export type Config = {
|
|
26
|
+
debug?: boolean,
|
|
27
|
+
bufferLimit: number,
|
|
26
28
|
dev: {
|
|
27
29
|
level: TLogProfile,
|
|
28
30
|
},
|
|
@@ -77,14 +79,35 @@ export type TQueryLogs = ChannelInfos & {
|
|
|
77
79
|
time: number,
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
export type TLog =
|
|
82
|
+
export type TLog = ILogObject & ChannelInfos
|
|
83
|
+
|
|
84
|
+
/*----------------------------------
|
|
85
|
+
- TYPES: BUG REPORT
|
|
86
|
+
----------------------------------*/
|
|
87
|
+
export type ServerBug = {
|
|
88
|
+
// Context
|
|
89
|
+
hash: string,
|
|
90
|
+
date: Date, // Timestamp
|
|
91
|
+
channelType?: string,
|
|
92
|
+
channelId?: string,
|
|
81
93
|
|
|
94
|
+
user: string | null | undefined,
|
|
95
|
+
ip: string | null | undefined,
|
|
96
|
+
|
|
97
|
+
// Error
|
|
98
|
+
error: Error,
|
|
99
|
+
stacktrace: string,
|
|
100
|
+
logs: string,
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
/*----------------------------------
|
|
85
104
|
- CONST
|
|
86
105
|
----------------------------------*/
|
|
87
106
|
|
|
107
|
+
const LogPrefix = '[console]'
|
|
108
|
+
|
|
109
|
+
const errorMailInterval = (1 * 60 * 60 * 1000); // 1 hour
|
|
110
|
+
|
|
88
111
|
const logFields = [
|
|
89
112
|
'date',
|
|
90
113
|
'logLevelId',
|
|
@@ -99,7 +122,7 @@ const logFields = [
|
|
|
99
122
|
'lineNumber',
|
|
100
123
|
'argumentsArray',
|
|
101
124
|
'stack',
|
|
102
|
-
]
|
|
125
|
+
] as const
|
|
103
126
|
|
|
104
127
|
/*----------------------------------
|
|
105
128
|
- LOGGER
|
|
@@ -111,13 +134,14 @@ export default class Console extends Service<Config, Hooks, Application> {
|
|
|
111
134
|
|
|
112
135
|
// Services
|
|
113
136
|
public logger!: Logger;
|
|
114
|
-
public bugReport = new BugReporter(this);
|
|
115
137
|
|
|
116
138
|
// Buffers
|
|
117
139
|
public logs: TLog[] = [];
|
|
118
140
|
public clients: TGuestLogs[] = [];
|
|
119
141
|
public requests: TRequestLogs[] = [];
|
|
120
142
|
public sqlQueries: TQueryLogs[] = [];
|
|
143
|
+
// Bug ID => Timestamp latest send
|
|
144
|
+
private sentBugs: {[bugId: string]: number} = {};
|
|
121
145
|
|
|
122
146
|
// Adapters
|
|
123
147
|
public log = console.log;
|
|
@@ -159,20 +183,75 @@ export default class Console extends Service<Config, Hooks, Application> {
|
|
|
159
183
|
fatal: this.logEntry.bind(this),
|
|
160
184
|
}, envConfig.level);
|
|
161
185
|
|
|
162
|
-
setInterval(() => this.clean(),
|
|
186
|
+
setInterval(() => this.clean(), 10000);
|
|
163
187
|
|
|
164
188
|
// Send email report
|
|
165
|
-
this.app.on('error',
|
|
189
|
+
this.app.on('error', this.createBugReport.bind(this));
|
|
166
190
|
}
|
|
167
191
|
|
|
168
192
|
private clean() {
|
|
169
|
-
|
|
193
|
+
this.config.debug && console.log(LogPrefix, `Clean logs buffer. Current size:`, this.logs.length, '/', this.config.bufferLimit);
|
|
194
|
+
const bufferOverflow = this.logs.length - this.config.bufferLimit;
|
|
195
|
+
if (bufferOverflow > 0)
|
|
196
|
+
this.logs = this.logs.slice(bufferOverflow);
|
|
170
197
|
}
|
|
171
198
|
|
|
172
199
|
/*----------------------------------
|
|
173
200
|
- LOGGING
|
|
174
201
|
----------------------------------*/
|
|
175
202
|
|
|
203
|
+
public async createBugReport( error: Error, request?: ServerRequest ) {
|
|
204
|
+
|
|
205
|
+
// Print the error so it's accessible via logs
|
|
206
|
+
if (error instanceof SqlError) {
|
|
207
|
+
let printedQuery: string;
|
|
208
|
+
try {
|
|
209
|
+
printedQuery = this.printSql( error.query );
|
|
210
|
+
} catch (error) {
|
|
211
|
+
printedQuery = 'Failed to print query:' + (error || 'unknown error');
|
|
212
|
+
}
|
|
213
|
+
console.error(`Error caused by this query:`, printedQuery);
|
|
214
|
+
}
|
|
215
|
+
console.error(LogPrefix, `Sending bug report for the following error:`, error);
|
|
216
|
+
|
|
217
|
+
// Prevent spamming the mailbox if infinite loop
|
|
218
|
+
const bugId = ['server', request?.user?.name, undefined, error.message].filter(e => !!e).join('::');
|
|
219
|
+
const lastSending = this.sentBugs[bugId];
|
|
220
|
+
this.sentBugs[bugId] = Date.now();
|
|
221
|
+
const shouldSendReport = lastSending === undefined || lastSending < Date.now() - errorMailInterval;
|
|
222
|
+
if (!shouldSendReport)
|
|
223
|
+
return;
|
|
224
|
+
|
|
225
|
+
// Get context
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const hash = uuid();
|
|
228
|
+
const { channelType, channelId } = this.getChannel();
|
|
229
|
+
|
|
230
|
+
// On envoi l'email avant l'insertion dans bla bdd
|
|
231
|
+
// Car cette denrière a plus de chances de provoquer une erreur
|
|
232
|
+
const logsHtml = this.printHtml(
|
|
233
|
+
this.logs.filter(e => e.channelId === channelId),
|
|
234
|
+
true
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const bugReport: ServerBug = {
|
|
238
|
+
// Context
|
|
239
|
+
hash: hash,
|
|
240
|
+
date: now,
|
|
241
|
+
channelType,
|
|
242
|
+
channelId,
|
|
243
|
+
// User
|
|
244
|
+
user: request?.user?.email,
|
|
245
|
+
ip: request?.ip,
|
|
246
|
+
// Error
|
|
247
|
+
error,
|
|
248
|
+
stacktrace: error.stack || error.message,
|
|
249
|
+
logs: logsHtml
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await this.runHook('bugReport', bugReport);
|
|
253
|
+
}
|
|
254
|
+
|
|
176
255
|
public getChannel() {
|
|
177
256
|
return context.getStore() || {
|
|
178
257
|
channelType: 'master',
|
|
@@ -182,6 +261,7 @@ export default class Console extends Service<Config, Hooks, Application> {
|
|
|
182
261
|
|
|
183
262
|
private logEntry(entry: ILogObject) {
|
|
184
263
|
|
|
264
|
+
// Don't keep logs from the admin sashboard
|
|
185
265
|
const [channelType, channelId] = entry.requestId?.split(':') || ['master'];
|
|
186
266
|
if (entry.requestId === 'admin')
|
|
187
267
|
return;
|
|
@@ -192,22 +272,6 @@ export default class Console extends Service<Config, Hooks, Application> {
|
|
|
192
272
|
for (const k of logFields)
|
|
193
273
|
miniLog[k] = entry[k];
|
|
194
274
|
|
|
195
|
-
// remove webpack path
|
|
196
|
-
if (miniLog.filePath !== undefined) {
|
|
197
|
-
|
|
198
|
-
const appPrefix = '/webpack:/' + this.app.pkg.name + '/src/';
|
|
199
|
-
const appPrefixIndex = miniLog.filePath.indexOf(appPrefix);
|
|
200
|
-
|
|
201
|
-
const corePrefix = '/webpack:/' + this.app.pkg.name + '/node_modules/5htp-core/src/';
|
|
202
|
-
const corePrefixIndex = miniLog.filePath.indexOf(corePrefix);
|
|
203
|
-
|
|
204
|
-
if (appPrefixIndex !== -1)
|
|
205
|
-
miniLog.filePath = '@/' + miniLog.filePath.substring(appPrefixIndex + appPrefix.length);
|
|
206
|
-
else if (corePrefixIndex !== -1)
|
|
207
|
-
miniLog.filePath = '@' + miniLog.filePath.substring(corePrefixIndex + corePrefix.length);
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
275
|
this.logs.push(miniLog as TLog);
|
|
212
276
|
}
|
|
213
277
|
|
|
@@ -285,11 +349,30 @@ export default class Console extends Service<Config, Hooks, Application> {
|
|
|
285
349
|
if (channelId !== undefined)
|
|
286
350
|
filters.channelId = channelId;
|
|
287
351
|
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
352
|
+
const entries: TLog[] = []
|
|
353
|
+
for (const log of this.logs) {
|
|
354
|
+
|
|
355
|
+
// Filters
|
|
356
|
+
if (!(log.channelId === channelId && log.channelType === channelType))
|
|
357
|
+
continue;
|
|
358
|
+
|
|
359
|
+
// Remove path prefixs
|
|
360
|
+
if (log.filePath !== undefined) {
|
|
361
|
+
|
|
362
|
+
const appPrefix = '/webpack:/' + this.app.pkg.name + '/src/';
|
|
363
|
+
const appPrefixIndex = log.filePath.indexOf(appPrefix);
|
|
364
|
+
|
|
365
|
+
const corePrefix = '/webpack:/' + this.app.pkg.name + '/node_modules/5htp-core/src/';
|
|
366
|
+
const corePrefixIndex = log.filePath.indexOf(corePrefix);
|
|
367
|
+
|
|
368
|
+
if (appPrefixIndex !== -1)
|
|
369
|
+
log.filePath = '@/' + log.filePath.substring(appPrefixIndex + appPrefix.length);
|
|
370
|
+
else if (corePrefixIndex !== -1)
|
|
371
|
+
log.filePath = '@' + log.filePath.substring(corePrefixIndex + corePrefix.length);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
291
374
|
|
|
292
|
-
return this.printHtml(
|
|
375
|
+
return this.printHtml( entries.reverse() );
|
|
293
376
|
}
|
|
294
377
|
|
|
295
378
|
public printHtml(logs: TLog[], full: boolean = false): string {
|
|
@@ -8,6 +8,7 @@ import mysql from 'mysql2/promise';
|
|
|
8
8
|
// Core: general
|
|
9
9
|
import Application from '@server/app';
|
|
10
10
|
import Service from '@server/app/service';
|
|
11
|
+
import { Anomaly } from '@common/errors';
|
|
11
12
|
|
|
12
13
|
// Core: specific
|
|
13
14
|
import { SqlError } from './debug';
|
|
@@ -26,20 +27,17 @@ const LogPrefix = '[database][connection]';
|
|
|
26
27
|
- SERVICE CONFIG
|
|
27
28
|
----------------------------------*/
|
|
28
29
|
|
|
30
|
+
type ConnectionConfig = {
|
|
31
|
+
name: string,
|
|
32
|
+
databases: string[],
|
|
33
|
+
host: string,
|
|
34
|
+
port: number,
|
|
35
|
+
login: string,
|
|
36
|
+
password: string,
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export type DatabaseServiceConfig = {
|
|
30
|
-
|
|
31
|
-
dev: {
|
|
32
|
-
host: string,
|
|
33
|
-
port: number,
|
|
34
|
-
login: string,
|
|
35
|
-
password: string,
|
|
36
|
-
},
|
|
37
|
-
prod: {
|
|
38
|
-
host: string,
|
|
39
|
-
port: number,
|
|
40
|
-
login: string,
|
|
41
|
-
password: string,
|
|
42
|
-
}
|
|
40
|
+
connections: ConnectionConfig[]
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
export type THooks = {
|
|
@@ -71,10 +69,11 @@ type TSelectQueryResult = any;
|
|
|
71
69
|
/*----------------------------------
|
|
72
70
|
- SERVICES
|
|
73
71
|
----------------------------------*/
|
|
74
|
-
export default class
|
|
72
|
+
export default class DatabaseManager extends Service<DatabaseServiceConfig, THooks, Application> {
|
|
75
73
|
|
|
76
74
|
private initialized = false;
|
|
77
75
|
public connection!: mysql.Pool;
|
|
76
|
+
public connectionConfig?: ConnectionConfig;
|
|
78
77
|
|
|
79
78
|
public tables: TDatabasesList = {};
|
|
80
79
|
public metas = new MetadataParser(this);
|
|
@@ -92,37 +91,47 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
|
|
|
92
91
|
|
|
93
92
|
this.initialized = false;
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
// Try to connect to one of the databases
|
|
95
|
+
const connectionErrors: string[] = []
|
|
96
|
+
for (const connectionConfig of this.config.connections){
|
|
97
|
+
try {
|
|
98
|
+
await this.connect(connectionConfig)
|
|
99
|
+
break;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn(LogPrefix, `Failed to connect to ${connectionConfig.name}: ` + error);
|
|
102
|
+
connectionErrors.push(connectionConfig.name + ': ' + error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
// Coudnt connect to any database
|
|
107
|
+
if (this.connectionConfig === undefined)
|
|
108
|
+
throw new Anomaly(`Couldnt connect to any database.`, { connectionErrors });
|
|
98
109
|
|
|
99
|
-
|
|
110
|
+
// Disconnect from the database when the app is terminated
|
|
111
|
+
this.app.on('cleanup', () => this.disconnect());
|
|
100
112
|
|
|
113
|
+
// Ready to make queries
|
|
101
114
|
this.initialized = true;
|
|
102
|
-
|
|
103
115
|
}
|
|
104
116
|
|
|
105
|
-
public async
|
|
117
|
+
public async disconnect() {
|
|
106
118
|
return this.connection.end();
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
/*----------------------------------
|
|
110
122
|
- INIT
|
|
111
123
|
----------------------------------*/
|
|
112
|
-
public async connect() {
|
|
124
|
+
public async connect(config: ConnectionConfig) {
|
|
113
125
|
|
|
114
|
-
console.info(LogPrefix, `
|
|
115
|
-
|
|
116
|
-
const creds = this.config[ this.app.env.profile ];
|
|
117
|
-
|
|
118
|
-
return await mysql.createPool({
|
|
126
|
+
console.info(LogPrefix, `Trying to connect to ${config.name} ...`);
|
|
127
|
+
this.connection = mysql.createPool({
|
|
119
128
|
|
|
120
129
|
// Identification
|
|
121
|
-
host:
|
|
122
|
-
port:
|
|
123
|
-
user:
|
|
124
|
-
password:
|
|
125
|
-
database:
|
|
130
|
+
host: config.host,
|
|
131
|
+
port: config.port,
|
|
132
|
+
user: config.login,
|
|
133
|
+
password: config.password,
|
|
134
|
+
database: config.databases[0],
|
|
126
135
|
|
|
127
136
|
// Pool
|
|
128
137
|
waitForConnections: true,
|
|
@@ -147,7 +156,12 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
|
|
|
147
156
|
//console.info(LogPrefix, 'queryFormat', query);
|
|
148
157
|
return query;
|
|
149
158
|
}
|
|
150
|
-
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
this.connectionConfig = config;
|
|
162
|
+
|
|
163
|
+
this.tables = await this.metas.load( config.databases );
|
|
164
|
+
console.info(LogPrefix, `Successfully connected to ${config.name}.`);
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
private typeCast( field: mysql.Field, next: Function ) {
|
|
@@ -226,13 +240,16 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
|
|
|
226
240
|
|
|
227
241
|
public getTable( path: string ): TMetasTable {
|
|
228
242
|
|
|
243
|
+
if (this.connectionConfig === undefined)
|
|
244
|
+
throw new Error(`No connection has been initialised.`);
|
|
245
|
+
|
|
229
246
|
// Parse path
|
|
230
247
|
let db: string, table: string;
|
|
231
248
|
if (path.includes('.'))
|
|
232
249
|
([db, table] = path.split('.'));
|
|
233
250
|
else {
|
|
234
251
|
// Only the table = use the main database (first of the list in the config)
|
|
235
|
-
db = this.
|
|
252
|
+
db = this.connectionConfig.databases[0];
|
|
236
253
|
table = path;
|
|
237
254
|
}
|
|
238
255
|
|
|
@@ -27,7 +27,7 @@ import BaseRouter, {
|
|
|
27
27
|
TRouteOptions, defaultOptions
|
|
28
28
|
} from '@common/router';
|
|
29
29
|
import { buildRegex, getRegisterPageArgs } from '@common/router/register';
|
|
30
|
-
import { layoutsList } from '@common/router/layouts';
|
|
30
|
+
import { layoutsList, getLayout } from '@common/router/layouts';
|
|
31
31
|
import { TFetcherList, TFetcher } from '@common/router/request/api';
|
|
32
32
|
import type { TFrontRenderer } from '@common/router/response/page';
|
|
33
33
|
import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@client/services/router';
|
|
@@ -147,7 +147,6 @@ export default class ServerRouter<
|
|
|
147
147
|
// Since route registering requires all services to be ready,
|
|
148
148
|
// We load routes only when all services are ready
|
|
149
149
|
this.app.on('ready', async () => {
|
|
150
|
-
|
|
151
150
|
// Use require to avoid circular references
|
|
152
151
|
this.registerRoutes([
|
|
153
152
|
...require("metas:@/server/routes/**/*.ts"),
|
|
@@ -228,9 +227,13 @@ export default class ServerRouter<
|
|
|
228
227
|
options: TRoute["options"],
|
|
229
228
|
renderer: TFrontRenderer<{}, { message: string }>
|
|
230
229
|
) {
|
|
230
|
+
|
|
231
|
+
// Automatic layout form the nearest _layout folder
|
|
232
|
+
const layout = getLayout('Error ' + code, options);
|
|
233
|
+
|
|
231
234
|
this.errors[code] = {
|
|
232
235
|
code,
|
|
233
|
-
controller: (context: TRouterContext<this>) => new Page(null, renderer, context),
|
|
236
|
+
controller: (context: TRouterContext<this>) => new Page(null, renderer, context, layout),
|
|
234
237
|
options
|
|
235
238
|
};
|
|
236
239
|
}
|
|
@@ -503,7 +506,7 @@ declare type Routes = {
|
|
|
503
506
|
return response;
|
|
504
507
|
}
|
|
505
508
|
|
|
506
|
-
throw new NotFound(
|
|
509
|
+
throw new NotFound();
|
|
507
510
|
}
|
|
508
511
|
|
|
509
512
|
private async resolveApiBatch( fetchers: TFetcherList, request: ServerRequest<this> ) {
|
|
@@ -542,9 +545,14 @@ declare type Routes = {
|
|
|
542
545
|
// Rapport / debug
|
|
543
546
|
if (code === 500) {
|
|
544
547
|
|
|
548
|
+
// Report error
|
|
545
549
|
await this.app.runHook('error', e, request);
|
|
546
550
|
|
|
547
|
-
//
|
|
551
|
+
// Don't exose technical errors to users
|
|
552
|
+
if (this.app.env.profile === 'prod')
|
|
553
|
+
e.message = "We encountered an internal error, and our team has just been notified. Sorry for the inconvenience.";
|
|
554
|
+
|
|
555
|
+
// Pour déboguer les erreurs HTTP
|
|
548
556
|
} else if (this.app.env.profile === "dev")
|
|
549
557
|
console.warn(e);
|
|
550
558
|
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// Npm
|
|
6
|
-
import React from 'react';
|
|
7
|
-
|
|
8
|
-
// Core
|
|
9
|
-
|
|
10
|
-
// Core components
|
|
11
|
-
import Button, { Props as ButtonProps } from '@client/components/button';
|
|
12
|
-
import useHeader from '@client/pages/useHeader';
|
|
13
|
-
|
|
14
|
-
/*----------------------------------
|
|
15
|
-
- CONTROLEUR
|
|
16
|
-
----------------------------------*/
|
|
17
|
-
import './index.less';
|
|
18
|
-
export default ({ headline, subtitle, cta, preview }: {
|
|
19
|
-
|
|
20
|
-
headline: string,
|
|
21
|
-
subtitle: string,
|
|
22
|
-
cta: ButtonProps,
|
|
23
|
-
preview: [string, string]
|
|
24
|
-
|
|
25
|
-
}) => {
|
|
26
|
-
|
|
27
|
-
useHeader({
|
|
28
|
-
title: headline,
|
|
29
|
-
subtitle: subtitle,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
/*----------------------------------
|
|
33
|
-
- RENDER
|
|
34
|
-
----------------------------------*/
|
|
35
|
-
return (
|
|
36
|
-
<div id="landing">
|
|
37
|
-
|
|
38
|
-
<header>
|
|
39
|
-
<div class="text card">
|
|
40
|
-
<h1>{headline}</h1>
|
|
41
|
-
<p>
|
|
42
|
-
{subtitle}
|
|
43
|
-
</p>
|
|
44
|
-
<Button type="guide" {...cta} />
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<figure>
|
|
48
|
-
<img class="image" src={preview[0]} />
|
|
49
|
-
<legend>{preview[1]}</legend>
|
|
50
|
-
</figure>
|
|
51
|
-
</header>
|
|
52
|
-
|
|
53
|
-
</div>
|
|
54
|
-
)
|
|
55
|
-
}
|