diff --git a/packages/backend/src/Application.ts b/packages/backend/src/Application.ts index ed0c6c5aa..a715f8833 100644 --- a/packages/backend/src/Application.ts +++ b/packages/backend/src/Application.ts @@ -65,6 +65,7 @@ import { LiveL2TransactionDownloader } from './core/sync/LiveL2TransactionDownlo import { SyncScheduler } from './core/sync/SyncScheduler' import { TransactionStatusService } from './core/TransactionStatusService' import { TransactionValidator } from './core/TransactionValidator' +import { TutorialService } from './core/TutorialService' import { UserService } from './core/UserService' import { AssetRepository } from './peripherals/database/AssetRepository' import { BlockRepository } from './peripherals/database/BlockRepository' @@ -206,6 +207,7 @@ export class Application { userTransactionRepository, logger ) + const tutorialService = new TutorialService() const userRegistrationCollector = new UserRegistrationCollector( ethereumClient, @@ -592,6 +594,7 @@ export class Application { pageContextService, assetDetailsService, forcedTradeOfferViewService, + tutorialService, userTransactionRepository, forcedTradeOfferRepository, l2TransactionRepository, @@ -680,7 +683,10 @@ export class Application { config.starkex.contracts.perpetual ) - const tutorialController = new TutorialController(pageContextService) + const tutorialController = new TutorialController( + pageContextService, + tutorialService + ) const apiServer = new ApiServer(config.port, logger, { routers: [ diff --git a/packages/backend/src/api/controllers/HomeController.ts b/packages/backend/src/api/controllers/HomeController.ts index 620d68416..49b6070a8 100644 --- a/packages/backend/src/api/controllers/HomeController.ts +++ b/packages/backend/src/api/controllers/HomeController.ts @@ -11,6 +11,7 @@ import { L2TransactionTypesToExclude } from '../../config/starkex/StarkexConfig' import { AssetDetailsService } from '../../core/AssetDetailsService' import { ForcedTradeOfferViewService } from '../../core/ForcedTradeOfferViewService' import { PageContextService } from '../../core/PageContextService' +import { TutorialService } from '../../core/TutorialService' import { PaginationOptions } from '../../model/PaginationOptions' import { ForcedTradeOfferRepository } from '../../peripherals/database/ForcedTradeOfferRepository' import { L2TransactionRepository } from '../../peripherals/database/L2TransactionRepository' @@ -38,6 +39,7 @@ export class HomeController { private readonly pageContextService: PageContextService, private readonly assetDetailsService: AssetDetailsService, private readonly forcedTradeOfferViewService: ForcedTradeOfferViewService, + private readonly tutorialService: TutorialService, private readonly userTransactionRepository: UserTransactionRepository, private readonly forcedTradeOfferRepository: ForcedTradeOfferRepository, private readonly l2TransactionRepository: L2TransactionRepository, @@ -96,6 +98,8 @@ export class HomeController { limit: stateUpdatesLimit, }) + const tutorials = this.tutorialService.getTutorials() + const assetDetailsMap = await this.assetDetailsService.getAssetDetailsMap({ userTransactions: forcedUserTransactions, }) @@ -132,7 +136,7 @@ export class HomeController { const content = renderHomePage({ context, - tutorials: [], // explicitly no tutorials + tutorials, l2Transactions: l2Transactions.map(l2TransactionToEntry), statistics, stateUpdates: stateUpdateEntries, diff --git a/packages/backend/src/api/controllers/TutorialController.ts b/packages/backend/src/api/controllers/TutorialController.ts index 0b616aebd..34000b7b9 100644 --- a/packages/backend/src/api/controllers/TutorialController.ts +++ b/packages/backend/src/api/controllers/TutorialController.ts @@ -1,12 +1,38 @@ -import { renderTutorialPage } from '@explorer/frontend' +import { renderTutorialPage, renderTutorialsPage } from '@explorer/frontend' import { UserDetails } from '@explorer/shared' import { PageContextService } from '../../core/PageContextService' +import { TutorialService } from '../../core/TutorialService' import { getHtmlFromMarkdown } from '../../utils/markdown/getHtmlFromMarkdown' import { ControllerResult } from './ControllerResult' export class TutorialController { - constructor(private readonly pageContextService: PageContextService) {} + constructor( + private readonly pageContextService: PageContextService, + private readonly tutorialsService: TutorialService + ) {} + + async getTutorialsPage( + givenUser: Partial + ): Promise { + const context = await this.pageContextService.getPageContext(givenUser) + const tutorials = this.tutorialsService.getTutorials() + + if (!tutorials) { + return { + type: 'not found', + message: 'There are no tutorials available', + } + } + + return { + type: 'success', + content: renderTutorialsPage({ + context, + tutorials, + }), + } + } async getTutorialPage( givenUser: Partial, diff --git a/packages/backend/src/api/routers/FrontendRouter.ts b/packages/backend/src/api/routers/FrontendRouter.ts index b592d70dd..0e48d292c 100644 --- a/packages/backend/src/api/routers/FrontendRouter.ts +++ b/packages/backend/src/api/routers/FrontendRouter.ts @@ -473,6 +473,11 @@ export function createFrontendRouter( ) ) } + router.get('/tutorials', async (ctx) => { + const givenUser = getGivenUser(ctx) + const result = await tutorialController.getTutorialsPage(givenUser) + applyControllerResult(ctx, result) + }) router.get( '/tutorials/:slug', diff --git a/packages/backend/src/content/tutorials/example.md b/packages/backend/src/content/tutorials/example.md index 14653b515..e69de29bb 100644 --- a/packages/backend/src/content/tutorials/example.md +++ b/packages/backend/src/content/tutorials/example.md @@ -1,112 +0,0 @@ - - -# h1 Heading - -## h2 Heading - -### h3 Heading - -#### h4 Heading - -##### h5 Heading - -###### h6 Heading - -## Horizontal Rules - ---- - ---- - ---- - -## Emphasis - -**This is bold text** - -**This is bold text** - -_This is italic text_ - -_This is italic text_ - -~~Strikethrough~~ - -## Blockquotes - -> Blockquotes can also be nested... -> -> > ...by using additional greater-than signs right next to each other... -> > -> > > ...or with spaces between arrows. - -## Lists - -Unordered - -- Create a list by starting a line with `+`, `-`, or `*` -- Sub-lists are made by indenting 2 spaces: - - Marker character change forces new list start: - - Ac tristique libero volutpat at - * Facilisis in pretium nisl aliquet - - Nulla volutpat aliquam velit -- Very easy! - -Ordered - -1. Lorem ipsum dolor sit amet -2. Consectetur adipiscing elit -3. Integer molestie lorem at massa - -4. You can use sequential numbers... -5. ...or keep all the numbers as `1.` - -Start numbering with offset: - -57. foo -1. bar - -## Code - -Inline `code` - -Indented code - - // Some comments - line 1 of code - line 2 of code - line 3 of code - -Block code "fences" - -``` -Sample text here... -``` - -## Tables - -| Option | Description | -| ------ | ------------------------------------------------------------------------- | -| data | path to data files to supply the data that will be passed into templates. | -| engine | engine to be used for processing templates. Handlebars is the default. | -| ext | extension to be used for dest files. | - -Right aligned columns - -| Option | Description | -| -----: | ------------------------------------------------------------------------: | -| data | path to data files to supply the data that will be passed into templates. | -| engine | engine to be used for processing templates. Handlebars is the default. | -| ext | extension to be used for dest files. | - -## Links - -[link text](http://dev.nodeca.com) - -[link with title](http://nodeca.github.io/pica/demo/ 'title text!') - -Autoconverted link https://github.com/nodeca/pica (enable linkify to see) - -## Images - -![Minion](https://octodex.github.com/images/minion.png) diff --git a/packages/backend/src/core/TutorialService.ts b/packages/backend/src/core/TutorialService.ts new file mode 100644 index 000000000..7e16fe05c --- /dev/null +++ b/packages/backend/src/core/TutorialService.ts @@ -0,0 +1,19 @@ +import { HomeTutorialEntry } from '@explorer/frontend' +import fs from 'fs' + +export class TutorialService { + constructor() {} + + getTutorials(): HomeTutorialEntry[] { + const files = fs.readdirSync('src/content/tutorials') + + return files.map((filename) => { + const filenameWithoutExt = filename.replace('.md', '') + return { + title: filenameWithoutExt.replace('-', ' '), + imageUrl: `/images/${filenameWithoutExt}.jpg`, + slug: filenameWithoutExt.toLowerCase(), + } + }) + } +} diff --git a/packages/frontend/src/preview/data/tutorial.ts b/packages/frontend/src/preview/data/tutorial.ts index c551470ea..8dbe72676 100644 --- a/packages/frontend/src/preview/data/tutorial.ts +++ b/packages/frontend/src/preview/data/tutorial.ts @@ -1,3 +1,5 @@ +import { HomeTutorialEntry } from '../../view' + /* eslint-disable no-useless-escape */ export const tutorial = `

h1 Heading

h2 Heading

@@ -132,3 +134,26 @@ line 3 of code

Images

Minion ` + +export const tutorials: HomeTutorialEntry[] = [ + { + title: 'Learn how to use StarkEx Explorer efficiently', + imageUrl: '/images/tutorial.jpg', + slug: 'learn-how-to-use-starkex-explorer-efficiently', + }, + { + title: 'All about forced transactions', + imageUrl: '/images/tutorial.jpg', + slug: 'all-about-forced-transactions', + }, + { + title: 'Stark key registration', + imageUrl: '/images/tutorial.jpg', + slug: 'stark-key-registration', + }, + { + title: 'Escape hatches explained', + imageUrl: '/images/tutorial.jpg', + slug: 'escape-hatch-explained', + }, +] diff --git a/packages/frontend/src/preview/routes.ts b/packages/frontend/src/preview/routes.ts index c15c92646..2e87a0dfa 100644 --- a/packages/frontend/src/preview/routes.ts +++ b/packages/frontend/src/preview/routes.ts @@ -48,6 +48,7 @@ import { renderFreezeRequestActionPage } from '../view/pages/forced-actions/Free import { renderPerpetualL2TransactionDetailsPage } from '../view/pages/l2-transaction/PerpetualL2TransactionDetailsPage' import { renderStateUpdateL2TransactionsPage } from '../view/pages/state-update/StateUpdateL2TransactionsPage' import { renderInitializeEscapePage } from '../view/pages/transaction/InitializeEscapePage' +import { renderTutorialsPage } from '../view/pages/tutorial/TutorialsPage' import { renderUserL2TransactionsPage } from '../view/pages/user/UserL2TransactionsPage' import { amountBucket, assetBucket } from './data/buckets' import { fakeCollateralAsset } from './data/collateralAsset' @@ -85,7 +86,7 @@ import { randomRecipient, userParty, } from './data/transactions' -import { tutorial } from './data/tutorial' +import { tutorial, tutorials } from './data/tutorial' import { randomEscapableEntry, randomUserAssetEntry, @@ -131,6 +132,7 @@ const routes: Route[] = [ ctx.body = renderHomePage({ context, + tutorials: tutorials, stateUpdates: repeat(10, randomHomeStateUpdateEntry), forcedTransactions: repeat(4, randomHomeForcedTransactionEntry), l2Transactions: [], @@ -155,6 +157,7 @@ const routes: Route[] = [ ctx.body = renderHomePage({ context, + tutorials: tutorials, stateUpdates: repeat(22, randomHomeStateUpdateEntry), l2Transactions: repeat(8, randomPerpetualL2TransactionEntry), forcedTransactions: repeat(4, randomHomeForcedTransactionEntry), @@ -1062,6 +1065,7 @@ const routes: Route[] = [ ctx.body = renderHomePage({ context, + tutorials: tutorials, stateUpdates: repeat(6, randomHomeStateUpdateEntry), forcedTransactions: repeat(6, randomHomeForcedTransactionEntry), l2Transactions: [], @@ -1105,6 +1109,7 @@ const routes: Route[] = [ context, stateUpdates: repeat(6, randomHomeStateUpdateEntry), forcedTransactions: repeat(6, randomHomeForcedTransactionEntry), + tutorials: tutorials, l2Transactions: [], statistics: { stateUpdateCount: 6315, @@ -1883,7 +1888,19 @@ const routes: Route[] = [ // #endregion // #region Tutorial { - path: '/tutorials/example', + path: '/tutorials', + description: 'List of all tutorials', + render: (ctx) => { + const context = getPerpetualPageContext(ctx) + ctx.body = renderTutorialsPage({ + context, + tutorials: tutorials, + }) + }, + }, + { + link: '/tutorials/example', + path: '/tutorials/:slug', description: 'Tutorial page', render: (ctx) => { const context = getPerpetualPageContext(ctx) diff --git a/packages/frontend/src/view/index.ts b/packages/frontend/src/view/index.ts index 5ac4a106c..192f57a58 100644 --- a/packages/frontend/src/view/index.ts +++ b/packages/frontend/src/view/index.ts @@ -2,25 +2,24 @@ export type { OfferEntry } from './components/tables/OffersTable' export type { PriceEntry } from './components/tables/PricesTable' export type { TransactionEntry } from './components/tables/TransactionsTable' export * from './pages/ErrorPage' +export * from './pages/MerkleProofPage' +export * from './pages/RawL2TransactionPage' export * from './pages/forced-actions/EscapeHatchActionPage' export * from './pages/forced-actions/FreezeRequestActionPage' export * from './pages/forced-actions/NewPerpetualForcedActionPage' export * from './pages/forced-actions/NewSpotForcedWithdrawalPage' -export type { HomeStateUpdateEntry } from './pages/home/components/HomeStateUpdatesTable' -export type { HomeTutorialEntry } from './pages/home/components/HomeTutorials' export * from './pages/home/HomeAvailableOffersPage' export * from './pages/home/HomeL2TransactionsPage' export * from './pages/home/HomePage' export * from './pages/home/HomeStateUpdatesPage' export * from './pages/home/HomeTransactionsPage' +export type { HomeStateUpdateEntry } from './pages/home/components/HomeStateUpdatesTable' export * from './pages/l2-transaction/PerpetualL2TransactionDetailsPage' -export * from './pages/MerkleProofPage' -export * from './pages/RawL2TransactionPage' -export type { StateUpdateBalanceChangeEntry } from './pages/state-update/components/StateUpdateBalanceChangesTable' export * from './pages/state-update/StateUpdateBalanceChangesPage' export * from './pages/state-update/StateUpdateL2TransactionsPage' export * from './pages/state-update/StateUpdatePage' export * from './pages/state-update/StateUpdateTransactionsPage' +export type { StateUpdateBalanceChangeEntry } from './pages/state-update/components/StateUpdateBalanceChangesTable' export * from './pages/transaction/FinalizeEscapeDetailsPage' export * from './pages/transaction/FreezeRequestDetailsPage' export * from './pages/transaction/InitializeEscapePage' @@ -29,9 +28,7 @@ export * from './pages/transaction/PerpetualForcedWithdrawalPage' export * from './pages/transaction/RegularWithdrawalPage' export * from './pages/transaction/SpotForcedWithdrawalPage' export * from './pages/tutorial/TutorialPage' -export type { UserAssetEntry } from './pages/user/components/UserAssetTable' -export type { UserBalanceChangeEntry } from './pages/user/components/UserBalanceChangesTable' -export * from './pages/user/components/UserQuickActionsTable' +export * from './pages/tutorial/TutorialsPage' export * from './pages/user/UserAssetsPage' export * from './pages/user/UserBalanceChangesPage' export * from './pages/user/UserL2TransactionsPage' @@ -40,3 +37,6 @@ export * from './pages/user/UserPage' export * from './pages/user/UserRecoverPage' export * from './pages/user/UserRegisterPage' export * from './pages/user/UserTransactionsPage' +export type { UserAssetEntry } from './pages/user/components/UserAssetTable' +export type { UserBalanceChangeEntry } from './pages/user/components/UserBalanceChangesTable' +export * from './pages/user/components/UserQuickActionsTable' diff --git a/packages/frontend/src/view/pages/home/HomePage.tsx b/packages/frontend/src/view/pages/home/HomePage.tsx index a28acd75c..2a5af7ba9 100644 --- a/packages/frontend/src/view/pages/home/HomePage.tsx +++ b/packages/frontend/src/view/pages/home/HomePage.tsx @@ -1,6 +1,7 @@ import { PageContext } from '@explorer/shared' import React from 'react' +import { HomeTutorialEntry } from '../..' import { Card } from '../../components/Card' import { ContentWrapper } from '../../components/page/ContentWrapper' import { Page } from '../../components/page/Page' @@ -26,15 +27,11 @@ import { HomeStateUpdatesTable, } from './components/HomeStateUpdatesTable' import { HomeStatistics, StatisticsEntry } from './components/HomeStatistics' -import { - DEFAULT_TUTORIALS, - HomeTutorialEntry, - HomeTutorials, -} from './components/HomeTutorials' +import { HomeTutorials } from './components/HomeTutorials' interface HomePageProps { context: PageContext - tutorials?: HomeTutorialEntry[] + tutorials: HomeTutorialEntry[] stateUpdates: HomeStateUpdateEntry[] l2Transactions: PerpetualL2TransactionEntry[] forcedTransactions: TransactionEntry[] @@ -47,8 +44,8 @@ export function renderHomePage(props: HomePageProps) { } function HomePage(props: HomePageProps) { - const tutorials = props.tutorials ?? DEFAULT_TUTORIALS - + const showViewAllTutorials = props.tutorials.length > 3 + console.log(props.tutorials) return ( 0 ? 'xl:col-span-2' : 'xl:col-span-3' + props.tutorials.length > 0 ? 'xl:col-span-2' : 'xl:col-span-3' } statistics={props.statistics} showL2Transactions={props.context.showL2Transactions} /> - {tutorials.length > 0 && ( + {props.tutorials.length > 0 && ( )} - {tutorials.length > 0 && ( - + {props.tutorials.length > 0 && ( + )} - {tutorials[0] && ( + {props.tutorials[2] && ( )} diff --git a/packages/frontend/src/view/pages/home/components/HomeSpotlightArticle.tsx b/packages/frontend/src/view/pages/home/components/HomeSpotlightArticle.tsx index b69e327c2..50651cbb4 100644 --- a/packages/frontend/src/view/pages/home/components/HomeSpotlightArticle.tsx +++ b/packages/frontend/src/view/pages/home/components/HomeSpotlightArticle.tsx @@ -1,12 +1,12 @@ import classNames from 'classnames' import React from 'react' +import { HomeTutorialEntry } from '../../..' import { Button } from '../../../components/Button' import { Card } from '../../../components/Card' -import { HomeTutorialEntry } from './HomeTutorials' interface HomeSpotlightArticleProps { - spotlightArticle: Omit + spotlightArticle: HomeTutorialEntry className?: string } @@ -18,14 +18,17 @@ export function HomeSpotlightArticle(props: HomeSpotlightArticleProps) { {props.spotlightArticle.title} - + ) } diff --git a/packages/frontend/src/view/pages/home/components/HomeTutorials.tsx b/packages/frontend/src/view/pages/home/components/HomeTutorials.tsx index 610e4aa92..a78f6578d 100644 --- a/packages/frontend/src/view/pages/home/components/HomeTutorials.tsx +++ b/packages/frontend/src/view/pages/home/components/HomeTutorials.tsx @@ -1,37 +1,16 @@ import classNames from 'classnames' import React from 'react' +import { HomeTutorialEntry } from '../../..' import { ArrowRightIcon } from '../../../assets/icons/ArrowIcon' import { Card } from '../../../components/Card' +import { Link } from '../../../components/Link' import { SectionHeading } from '../../../components/SectionHeading' -export const DEFAULT_TUTORIALS: HomeTutorialEntry[] = [ - { - title: 'Learn how to use StarkEx Explorer efficiently', - imageUrl: '/images/tutorial.jpg', - href: '/tutorials/example', - }, - { - title: 'All about forced transactions', - imageUrl: '/images/tutorial.jpg', - href: '/tutorials/example', - }, - { - title: 'Stark key registration', - imageUrl: '/images/tutorial.jpg', - href: '/tutorials/example', - }, -] - interface HomeTutorialsProps { className?: string tutorials: HomeTutorialEntry[] -} - -export interface HomeTutorialEntry { - title: string - imageUrl: string - href: string + showViewAll?: boolean } export function HomeTutorials(props: HomeTutorialsProps) { @@ -39,18 +18,25 @@ export function HomeTutorials(props: HomeTutorialsProps) {

View all + ) : ( + 'Learn how to use the StarkEx Explorer' + ) + } /> - + {props.tutorials.map((tutorial, i) => (

diff --git a/packages/frontend/src/view/pages/tutorial/TutorialsPage.tsx b/packages/frontend/src/view/pages/tutorial/TutorialsPage.tsx new file mode 100644 index 000000000..e76e8b419 --- /dev/null +++ b/packages/frontend/src/view/pages/tutorial/TutorialsPage.tsx @@ -0,0 +1,61 @@ +import { PageContext } from '@explorer/shared' +import React from 'react' + +import { Button } from '../../components/Button' +import { ContentWrapper } from '../../components/page/ContentWrapper' +import { Page } from '../../components/page/Page' +import { PageTitle } from '../../components/PageTitle' +import { reactToHtml } from '../../reactToHtml' + +export interface HomeTutorialEntry { + title: string + imageUrl: string + slug: string +} + +interface TutorialsPageProps { + context: PageContext + tutorials: HomeTutorialEntry[] +} + +export function renderTutorialsPage(props: TutorialsPageProps) { + return reactToHtml() +} + +export function TutorialsPage(props: TutorialsPageProps) { + return ( + + + Tutorials +

+ {props.tutorials.map((tutorial, i) => ( +
+ +
+

+ {tutorial.title} +

+ +
+
+ ))} +
+ + + ) +}