mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-04-29 15:37:48 +00:00
Compare commits
3 Commits
java-crede
...
fix-amm-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a078f09f21 | ||
|
|
7e934fe44d | ||
|
|
d945d6a5d6 |
@@ -1,13 +1,15 @@
|
|||||||
import { indexPages } from './plugins/index-pages.js';
|
import { indexPages } from './plugins/index-pages.js';
|
||||||
import { codeSamples } from './plugins/code-samples.js';
|
import { codeSamples } from './plugins/code-samples.js';
|
||||||
import { blogPosts } from './plugins/blog-posts.js';
|
import { blogPosts } from './plugins/blog-posts.js';
|
||||||
import { tutorialLanguages } from './plugins/tutorial-languages.js'
|
import { tutorialLanguages } from './plugins/tutorial-languages.js';
|
||||||
|
import { tutorialMetadata } from './plugins/tutorial-metadata.js';
|
||||||
|
|
||||||
export default function customPlugin() {
|
export default function customPlugin() {
|
||||||
const indexPagesInst = indexPages();
|
const indexPagesInst = indexPages();
|
||||||
const codeSamplesInst = codeSamples();
|
const codeSamplesInst = codeSamples();
|
||||||
const blogPostsInst = blogPosts();
|
const blogPostsInst = blogPosts();
|
||||||
const tutorialLanguagesInst = tutorialLanguages();
|
const tutorialLanguagesInst = tutorialLanguages();
|
||||||
|
const tutorialMetadataInst = tutorialMetadata();
|
||||||
|
|
||||||
|
|
||||||
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
|
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
|
||||||
@@ -18,12 +20,14 @@ export default function customPlugin() {
|
|||||||
await codeSamplesInst.processContent?.(content, actions);
|
await codeSamplesInst.processContent?.(content, actions);
|
||||||
await blogPostsInst.processContent?.(content, actions);
|
await blogPostsInst.processContent?.(content, actions);
|
||||||
await tutorialLanguagesInst.processContent?.(content, actions);
|
await tutorialLanguagesInst.processContent?.(content, actions);
|
||||||
|
await tutorialMetadataInst.processContent?.(content, actions);
|
||||||
},
|
},
|
||||||
afterRoutesCreated: async (content, actions) => {
|
afterRoutesCreated: async (content, actions) => {
|
||||||
await indexPagesInst.afterRoutesCreated?.(content, actions);
|
await indexPagesInst.afterRoutesCreated?.(content, actions);
|
||||||
await codeSamplesInst.afterRoutesCreated?.(content, actions);
|
await codeSamplesInst.afterRoutesCreated?.(content, actions);
|
||||||
await blogPostsInst.afterRoutesCreated?.(content, actions);
|
await blogPostsInst.afterRoutesCreated?.(content, actions);
|
||||||
await tutorialLanguagesInst.processContent?.(content, actions);
|
await tutorialLanguagesInst.processContent?.(content, actions);
|
||||||
|
await tutorialMetadataInst.processContent?.(content, actions);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to detect languages supported in tutorial pages by scanning for tab labels.
|
* Plugin to detect languages supported in tutorial pages.
|
||||||
|
*
|
||||||
|
* Detection methods (in priority order):
|
||||||
|
* 1. Tab labels in the markdown (for multi-language tutorials)
|
||||||
|
* 2. Filename patterns like "-js.md", "-py.md" (for single-language tutorials)
|
||||||
|
* 3. Title containing language name (for single-language tutorials)
|
||||||
|
*
|
||||||
* This creates shared data that maps tutorial paths to their supported languages.
|
* This creates shared data that maps tutorial paths to their supported languages.
|
||||||
*/
|
*/
|
||||||
export function tutorialLanguages() {
|
export function tutorialLanguages() {
|
||||||
@@ -21,7 +29,18 @@ export function tutorialLanguages() {
|
|||||||
for (const { relativePath } of tutorialFiles) {
|
for (const { relativePath } of tutorialFiles) {
|
||||||
try {
|
try {
|
||||||
const { data } = await cache.load(relativePath, 'markdown-ast')
|
const { data } = await cache.load(relativePath, 'markdown-ast')
|
||||||
const languages = extractLanguagesFromAst(data.ast)
|
|
||||||
|
// Try to detect languages from tab labels first
|
||||||
|
let languages = extractLanguagesFromAst(data.ast)
|
||||||
|
|
||||||
|
// Fallback: detect language from filename/title for single-language tutorials
|
||||||
|
if (languages.length === 0) {
|
||||||
|
const title = extractFirstHeading(data.ast) || ''
|
||||||
|
const fallbackLang = detectLanguageFromPathAndTitle(relativePath, title)
|
||||||
|
if (fallbackLang) {
|
||||||
|
languages = [fallbackLang]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (languages.length > 0) {
|
if (languages.length > 0) {
|
||||||
// Convert file path to URL path
|
// Convert file path to URL path
|
||||||
@@ -54,16 +73,31 @@ function extractLanguagesFromAst(ast) {
|
|||||||
const languages = new Set()
|
const languages = new Set()
|
||||||
|
|
||||||
visit(ast, (node) => {
|
visit(ast, (node) => {
|
||||||
// Look for tab nodes with a label attribute
|
if (!isNode(node)) return
|
||||||
if (isNode(node) && node.type === 'tag' && node.tag === 'tab') {
|
|
||||||
|
// Detect languages from tab labels
|
||||||
|
if (node.type === 'tag' && node.tag === 'tab') {
|
||||||
const label = node.attributes?.label
|
const label = node.attributes?.label
|
||||||
if (label) {
|
if (label) {
|
||||||
const normalizedLang = normalizeLanguage(label)
|
const normalized = normalizeLanguage(label)
|
||||||
if (normalizedLang) {
|
if (normalized) languages.add(normalized)
|
||||||
languages.add(normalizedLang)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect languages from code-snippet language attributes
|
||||||
|
if (node.type === 'tag' && node.tag === 'code-snippet') {
|
||||||
|
const lang = node.attributes?.language
|
||||||
|
if (lang) {
|
||||||
|
const normalized = normalizeLanguage(lang)
|
||||||
|
if (normalized) languages.add(normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect languages from fenced code blocks (```js, ```python, etc.)
|
||||||
|
if (node.type === 'fence' && node.attributes?.language) {
|
||||||
|
const normalized = normalizeLanguage(node.attributes.language)
|
||||||
|
if (normalized) languages.add(normalized)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return Array.from(languages)
|
return Array.from(languages)
|
||||||
@@ -98,6 +132,70 @@ function normalizeLanguage(label) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect language from file path and title for single-language tutorials.
|
||||||
|
* This is a fallback when no tab labels are found in the markdown.
|
||||||
|
*/
|
||||||
|
function detectLanguageFromPathAndTitle(relativePath, title) {
|
||||||
|
const pathLower = relativePath.toLowerCase()
|
||||||
|
const titleLower = (title || '').toLowerCase()
|
||||||
|
|
||||||
|
// Check filename suffixes like "-js.md", "-py.md"
|
||||||
|
if (pathLower.endsWith('-js.md') || pathLower.includes('-javascript.md') || pathLower.includes('-in-javascript.md')) {
|
||||||
|
return 'javascript'
|
||||||
|
}
|
||||||
|
if (pathLower.endsWith('-py.md') || pathLower.includes('-python.md') || pathLower.includes('-in-python.md')) {
|
||||||
|
return 'python'
|
||||||
|
}
|
||||||
|
if (pathLower.endsWith('-java.md') || pathLower.includes('-in-java.md')) {
|
||||||
|
return 'java'
|
||||||
|
}
|
||||||
|
if (pathLower.endsWith('-go.md') || pathLower.includes('-in-go.md') || pathLower.includes('-golang.md')) {
|
||||||
|
return 'go'
|
||||||
|
}
|
||||||
|
if (pathLower.endsWith('-php.md') || pathLower.includes('-in-php.md')) {
|
||||||
|
return 'php'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check title for language indicators
|
||||||
|
if (titleLower.includes('javascript') || titleLower.includes(' js ') || titleLower.endsWith(' js')) {
|
||||||
|
return 'javascript'
|
||||||
|
}
|
||||||
|
if (titleLower.includes('python')) {
|
||||||
|
return 'python'
|
||||||
|
}
|
||||||
|
if (titleLower.includes('java') && !titleLower.includes('javascript')) {
|
||||||
|
return 'java'
|
||||||
|
}
|
||||||
|
if (titleLower.includes('golang') || (titleLower.includes(' go ') || titleLower.endsWith(' go') || titleLower.includes('using go'))) {
|
||||||
|
return 'go'
|
||||||
|
}
|
||||||
|
if (titleLower.includes('php')) {
|
||||||
|
return 'php'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXIT = Symbol('Exit visitor')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the first heading from the markdown AST
|
||||||
|
*/
|
||||||
|
function extractFirstHeading(ast) {
|
||||||
|
let heading = null
|
||||||
|
|
||||||
|
visit(ast, (node) => {
|
||||||
|
if (!isNode(node)) return
|
||||||
|
if (node.type === 'heading') {
|
||||||
|
heading = getInnerText([node])
|
||||||
|
return EXIT
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return heading
|
||||||
|
}
|
||||||
|
|
||||||
function isNode(value) {
|
function isNode(value) {
|
||||||
return !!(value?.$$mdtype === 'Node')
|
return !!(value?.$$mdtype === 'Node')
|
||||||
}
|
}
|
||||||
@@ -105,14 +203,16 @@ function isNode(value) {
|
|||||||
function visit(node, visitor) {
|
function visit(node, visitor) {
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
|
||||||
visitor(node)
|
const res = visitor(node)
|
||||||
|
if (res === EXIT) return res
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
if (!child || typeof child === 'string') {
|
if (!child || typeof child === 'string') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
visit(child, visitor)
|
const res = visit(child, visitor)
|
||||||
|
if (res === EXIT) return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
207
@theme/plugins/tutorial-metadata.js
Normal file
207
@theme/plugins/tutorial-metadata.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { getInnerText } from '@redocly/realm/dist/markdoc/helpers/get-inner-text.js';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const PROJECT_ROOT = resolve(__dirname, '../..');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin to extract tutorial metadata including last modified dates.
|
||||||
|
* Uses Redocly's built-in git integration for dates (same as "Last updated" display).
|
||||||
|
* Only includes tutorials that appear in the sidebar navigation (sidebars.yaml).
|
||||||
|
* This creates shared data for displaying "What's New" tutorials and
|
||||||
|
* auto-generating tutorial sections on the landing page.
|
||||||
|
*/
|
||||||
|
export function tutorialMetadata() {
|
||||||
|
/** @type {import("@redocly/realm/dist/server/plugins/types").PluginInstance } */
|
||||||
|
const instance = {
|
||||||
|
processContent: async (actions, { fs, cache }) => {
|
||||||
|
try {
|
||||||
|
// Extract tutorial paths and categories from sidebars.yaml.
|
||||||
|
// Only tutorials present in the sidebar are included.
|
||||||
|
const { pageCategory, categories } = extractSidebarData();
|
||||||
|
|
||||||
|
/** @type {Array<{path: string, title: string, description: string, lastModified: string, category: string}>} */
|
||||||
|
const tutorials = [];
|
||||||
|
const allFiles = await fs.scan();
|
||||||
|
|
||||||
|
// Find all markdown files in tutorials directory
|
||||||
|
const tutorialFiles = allFiles.filter((file) =>
|
||||||
|
file.relativePath.match(/^docs[\/\\]tutorials[\/\\].*\.md$/)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const { relativePath } of tutorialFiles) {
|
||||||
|
try {
|
||||||
|
// Skip tutorials not present in sidebar navigation
|
||||||
|
const category = pageCategory.get(relativePath);
|
||||||
|
if (!category) continue;
|
||||||
|
|
||||||
|
const { data: { ast } } = await cache.load(relativePath, 'markdown-ast');
|
||||||
|
const { data: { frontmatter } } = await cache.load(relativePath, 'markdown-frontmatter');
|
||||||
|
|
||||||
|
// Get last modified date using Redocly's built-in git integration
|
||||||
|
const lastModified = await fs.getLastModified(relativePath);
|
||||||
|
if (!lastModified) continue; // Skip files without dates
|
||||||
|
|
||||||
|
// Extract title from first heading
|
||||||
|
const title = extractFirstHeading(ast) || '';
|
||||||
|
if (!title) continue;
|
||||||
|
|
||||||
|
// Get description from frontmatter or first paragraph
|
||||||
|
const description = frontmatter?.seo?.description || '';
|
||||||
|
|
||||||
|
// Convert file path to URL path
|
||||||
|
const urlPath = '/' + relativePath
|
||||||
|
.replace(/[\/\\]index\.md$/, '/')
|
||||||
|
.replace(/\.md$/, '/')
|
||||||
|
.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
tutorials.push({
|
||||||
|
path: urlPath,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
lastModified,
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
continue; // Skip files that can't be parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by last modified date (newest first) for "What's New"
|
||||||
|
tutorials.sort((a, b) =>
|
||||||
|
new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create shared data including sidebar-derived categories
|
||||||
|
actions.createSharedData('tutorial-metadata', { tutorials, categories });
|
||||||
|
actions.addRouteSharedData('/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
|
||||||
|
actions.addRouteSharedData('/ja/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
|
||||||
|
actions.addRouteSharedData('/es-es/docs/tutorials/', 'tutorial-metadata', 'tutorial-metadata');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[tutorial-metadata] Error:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the first heading from the markdown AST
|
||||||
|
*/
|
||||||
|
function extractFirstHeading(ast) {
|
||||||
|
let heading = null;
|
||||||
|
|
||||||
|
visit(ast, (node) => {
|
||||||
|
if (!isNode(node)) return;
|
||||||
|
if (node.type === 'heading') {
|
||||||
|
heading = getInnerText([node]);
|
||||||
|
return EXIT;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return heading;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNode(value) {
|
||||||
|
return !!(value?.$$mdtype === 'Node');
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXIT = Symbol('Exit visitor');
|
||||||
|
|
||||||
|
function visit(node, visitor) {
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const res = visitor(node);
|
||||||
|
if (res === EXIT) return res;
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (!child || typeof child === 'string') continue;
|
||||||
|
const res = visit(child, visitor);
|
||||||
|
if (res === EXIT) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tutorial page paths and categories from sidebars.yaml.
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* - pageCategory: Map of relativePath to category id (slug)
|
||||||
|
* - categories: Array of { id, title } in sidebar display order
|
||||||
|
*
|
||||||
|
* Top-level groups under the tutorials section become categories.
|
||||||
|
* Pages not inside a group (e.g. public-servers.md) are skipped.
|
||||||
|
*/
|
||||||
|
function extractSidebarData() {
|
||||||
|
/** @type {Map<string, string>} */
|
||||||
|
const pageCategory = new Map();
|
||||||
|
/** @type {Array<{id: string, title: string}>} */
|
||||||
|
const categories = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(resolve(PROJECT_ROOT, 'sidebars.yaml'), 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
let inTutorials = false;
|
||||||
|
let entryIndent = -1; // indent of the tutorials entry itself
|
||||||
|
let topItemIndent = -1; // indent of direct children (groups/pages)
|
||||||
|
let currentCategory = null; // current top-level group { id, title }
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const indent = line.search(/\S/);
|
||||||
|
|
||||||
|
// Detect the tutorials section
|
||||||
|
if (trimmed.includes('page: docs/tutorials/index.page.tsx')) {
|
||||||
|
inTutorials = true;
|
||||||
|
entryIndent = indent;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inTutorials) continue;
|
||||||
|
|
||||||
|
// Exit tutorials when we reach a sibling entry at the same indent
|
||||||
|
if (indent <= entryIndent && trimmed.startsWith('- ')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect the indent of top-level items (first `- ` under tutorials items)
|
||||||
|
if (topItemIndent === -1 && trimmed.startsWith('- ')) {
|
||||||
|
topItemIndent = indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level group - start a new category
|
||||||
|
if (indent === topItemIndent && trimmed.startsWith('- group:')) {
|
||||||
|
const title = trimmed.replace('- group:', '').trim();
|
||||||
|
const id = title.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
currentCategory = { id, title };
|
||||||
|
categories.push(currentCategory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level page (no group, e.g. public-servers.md) - reset current category
|
||||||
|
if (indent === topItemIndent && trimmed.startsWith('- page:')) {
|
||||||
|
currentCategory = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested page under a group - assign to current category
|
||||||
|
if (currentCategory) {
|
||||||
|
const pageMatch = trimmed.match(/^- page:\s+(docs\/tutorials\/\S+\.md)/);
|
||||||
|
if (pageMatch) {
|
||||||
|
pageCategory.set(pageMatch[1], currentCategory.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('[tutorial-metadata] Warning: Could not read sidebars.yaml:', String(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pageCategory, categories };
|
||||||
|
}
|
||||||
@@ -32,7 +32,8 @@ An example of the request format:
|
|||||||
"asset2": {
|
"asset2": {
|
||||||
"currency": "TST",
|
"currency": "TST",
|
||||||
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
||||||
}
|
},
|
||||||
|
"ledger_index": "validated"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
{% /tab %}
|
{% /tab %}
|
||||||
@@ -48,7 +49,8 @@ An example of the request format:
|
|||||||
"asset2": {
|
"asset2": {
|
||||||
"currency": "TST",
|
"currency": "TST",
|
||||||
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
||||||
}
|
},
|
||||||
|
"ledger_index": "validated"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -60,12 +62,14 @@ An example of the request format:
|
|||||||
|
|
||||||
The request includes the following parameters:
|
The request includes the following parameters:
|
||||||
|
|
||||||
| `Field` | Type | Required? | Description |
|
| `Field` | Type | Required? | Description |
|
||||||
|:--------------|:-----------------|:----------|-------------|
|
|:---------------|:---------------------|:----------|-------------|
|
||||||
| `account` | String - [Address][] | No | Show only LP Tokens held by this liquidity provider. |
|
| `account` | String - [Address][] | No | Show only LP Tokens held by this liquidity provider. |
|
||||||
| `amm_account` | String - [Address][] | No | The address of the AMM's special AccountRoot. (This is the `issuer` of the AMM's LP Tokens.) |
|
| `amm_account` | String - [Address][] | No | The address of the AMM's special AccountRoot. (This is the `issuer` of the AMM's LP Tokens.) |
|
||||||
| `asset` | Object | No | One of the assets of the AMM to look up, as an object with `currency` and `issuer` fields (omit `issuer` for XRP), like [currency amounts][Currency Amount]. |
|
| `asset` | Object | No | One of the assets of the AMM to look up, as an object with `currency` and `issuer` fields (omit `issuer` for XRP), like [currency amounts][Currency Amount]. |
|
||||||
| `asset2` | Object | No | The other of the assets of the AMM, as an object with `currency` and `issuer` fields (omit `issuer` for XRP), like [currency amounts][Currency Amount]. |
|
| `asset2` | Object | No | The other of the assets of the AMM, as an object with `currency` and `issuer` fields (omit `issuer` for XRP), like [currency amounts][Currency Amount]. |
|
||||||
|
| `ledger_hash` | String | No | The unique hash of the ledger version to use. See [Specifying Ledgers][]. |
|
||||||
|
| `ledger_index` | Number or String | No | The [ledger index][] of the ledger to use, or a shortcut string to choose a ledger automatically. See [Specifying Ledgers][]. |
|
||||||
|
|
||||||
You must specify _either_ `amm_account` or both `asset` and `asset2`.
|
You must specify _either_ `amm_account` or both `asset` and `asset2`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,52 @@
|
|||||||
import { useThemeHooks } from "@redocly/theme/core/hooks"
|
import { useThemeHooks } from "@redocly/theme/core/hooks"
|
||||||
import { Link } from "@redocly/theme/components/Link/Link"
|
import { Link } from "@redocly/theme/components/Link/Link"
|
||||||
|
import { useRef, useState } from "react"
|
||||||
|
|
||||||
|
type TutorialLanguagesMap = Record<string, string[]>
|
||||||
|
|
||||||
|
interface TutorialMetadataItem {
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
lastModified: string
|
||||||
|
category: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tutorial {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
path: string
|
||||||
|
// External community contribution fields (optional)
|
||||||
|
author?: { name: string; url: string }
|
||||||
|
github?: string
|
||||||
|
externalUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TutorialSection {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
tutorials: Tutorial[]
|
||||||
|
showFooter?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// External community contribution - manually curated with author/repo/demo info
|
||||||
|
interface PinnedExternalTutorial {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
author: { name: string; url: string }
|
||||||
|
github: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinned tutorial entry:
|
||||||
|
// - string: internal path (uses frontmatter title/description)
|
||||||
|
// - object with `path`: internal path with optional description override
|
||||||
|
// - PinnedExternalTutorial: external community contribution with author/repo/demo
|
||||||
|
type PinnedTutorial = string | { path: string; description?: string } | PinnedExternalTutorial
|
||||||
|
|
||||||
|
const MAX_WHATS_NEW = 3
|
||||||
|
const MAX_TUTORIALS_PER_SECTION = 6
|
||||||
|
|
||||||
export const frontmatter = {
|
export const frontmatter = {
|
||||||
seo: {
|
seo: {
|
||||||
@@ -19,242 +66,90 @@ const langIcons: Record<string, { src: string; alt: string }> = {
|
|||||||
xrpl: { src: "/img/logos/xrp-mark.svg", alt: "XRP Ledger" },
|
xrpl: { src: "/img/logos/xrp-mark.svg", alt: "XRP Ledger" },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type for the tutorial languages map from the plugin
|
// ── Section configuration -----------------------------------------------------------
|
||||||
type TutorialLanguagesMap = Record<string, string[]>
|
// Categories and their titles are auto-detected by the tutorial-metadata plugin.
|
||||||
|
// Use the config to customize the category titles, add descriptions, change the default category order, and pin tutorials.
|
||||||
interface Tutorial {
|
const sectionConfig: Record<string, {
|
||||||
title: string
|
title?: string
|
||||||
body?: string
|
description?: string
|
||||||
path: string
|
pinned?: PinnedTutorial[]
|
||||||
icon?: string // Single language icon (for single-language tutorials)
|
showFooter?: boolean
|
||||||
}
|
}> = {
|
||||||
|
"whats-new": {
|
||||||
interface TutorialSection {
|
title: "What's New",
|
||||||
id: string
|
description: "Recently added/updated tutorials to help you build on the XRP Ledger.",
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
tutorials: Tutorial[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Started tutorials -----------------
|
|
||||||
const getStartedTutorials: Tutorial[] = [
|
|
||||||
{
|
|
||||||
title: "JavaScript",
|
|
||||||
body: "Using the xrpl.js client library.",
|
|
||||||
path: "/docs/tutorials/get-started/get-started-javascript/",
|
|
||||||
icon: "javascript",
|
|
||||||
},
|
},
|
||||||
{
|
"get-started": {
|
||||||
title: "Python",
|
showFooter: true,
|
||||||
body: "Using xrpl.py, a pure Python library.",
|
title: "Get Started with SDKs",
|
||||||
path: "/docs/tutorials/get-started/get-started-python/",
|
description: "These tutorials walk you through the basics of building a very simple XRP Ledger-connected application using your favorite programming language.",
|
||||||
icon: "python",
|
pinned: [
|
||||||
|
{ path: "/docs/tutorials/get-started/get-started-javascript/", description: "Using the xrpl.js client library." },
|
||||||
|
{ path: "/docs/tutorials/get-started/get-started-python/", description: "Using xrpl.py, a pure Python library." },
|
||||||
|
{ path: "/docs/tutorials/get-started/get-started-go/", description: "Using xrpl-go, a pure Go library." },
|
||||||
|
{ path: "/docs/tutorials/get-started/get-started-java/", description: "Using xrpl4j, a pure Java library." },
|
||||||
|
{ path: "/docs/tutorials/get-started/get-started-php/", description: "Using the XRPL_PHP client library." },
|
||||||
|
{ path: "/docs/tutorials/get-started/get-started-http-websocket-apis/", description: "Access the XRP Ledger directly through the APIs of its core server." },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
"tokens": {
|
||||||
title: "Go",
|
|
||||||
body: "Using xrpl-go, a pure Go library.",
|
|
||||||
path: "/docs/tutorials/get-started/get-started-go/",
|
|
||||||
icon: "go",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Java",
|
|
||||||
body: "Using xrpl4j, a pure Java library.",
|
|
||||||
path: "/docs/tutorials/get-started/get-started-java/",
|
|
||||||
icon: "java",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PHP",
|
|
||||||
body: "Using the XRPL_PHP client library.",
|
|
||||||
path: "/docs/tutorials/get-started/get-started-php/",
|
|
||||||
icon: "php",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "HTTP & WebSocket APIs",
|
|
||||||
body: "Access the XRP Ledger directly through the APIs of its core server.",
|
|
||||||
path: "/docs/tutorials/get-started/get-started-http-websocket-apis/",
|
|
||||||
icon: "http",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Other tutorial sections -----------------
|
|
||||||
// Languages are auto-detected from the markdown files by the tutorial-languages plugin.
|
|
||||||
// Only specify `icon` for single-language tutorials without tabs.
|
|
||||||
const sections: TutorialSection[] = [
|
|
||||||
{
|
|
||||||
id: "tokens",
|
|
||||||
title: "Tokens",
|
|
||||||
description: "Create and manage tokens on the XRP Ledger.",
|
description: "Create and manage tokens on the XRP Ledger.",
|
||||||
tutorials: [
|
pinned: [
|
||||||
{
|
{ path: "/docs/tutorials/tokens/mpts/issue-a-multi-purpose-token/", description: "Issue new tokens using the v2 fungible token standard." },
|
||||||
title: "Issue a Multi-Purpose Token",
|
{ path: "/docs/tutorials/tokens/fungible-tokens/issue-a-fungible-token/", description: "Issue new tokens using the v1 fungible token standard." },
|
||||||
body: "Issue new tokens using the v2 fungible token standard.",
|
{ path: "/docs/tutorials/tokens/nfts/mint-and-burn-nfts-js/", description: "Create new NFTs, retrieve existing tokens, and burn the ones you no longer need." },
|
||||||
path: "/docs/tutorials/tokens/mpts/issue-a-multi-purpose-token/",
|
"/docs/tutorials/tokens/mpts/sending-mpts-in-javascript/",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Issue a Fungible Token",
|
|
||||||
body: "Issue new tokens using the v1 fungible token standard.",
|
|
||||||
path: "/docs/tutorials/tokens/fungible-tokens/issue-a-fungible-token/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Mint and Burn NFTs Using JavaScript",
|
|
||||||
body: "Create new NFTs, retrieve existing tokens, and burn the ones you no longer need.",
|
|
||||||
path: "/docs/tutorials/tokens/nfts/mint-and-burn-nfts-js/",
|
|
||||||
icon: "javascript",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
"payments": {
|
||||||
id: "payments",
|
|
||||||
title: "Payments",
|
|
||||||
description: "Transfer XRP and issued currencies using various payment types.",
|
description: "Transfer XRP and issued currencies using various payment types.",
|
||||||
tutorials: [
|
pinned: [
|
||||||
{
|
"/docs/tutorials/payments/send-xrp/",
|
||||||
title: "Send XRP",
|
"/docs/tutorials/payments/create-trust-line-send-currency-in-javascript/",
|
||||||
body: "Send a direct XRP payment to another account.",
|
"/docs/tutorials/payments/send-a-conditional-escrow/",
|
||||||
path: "/docs/tutorials/payments/send-xrp/",
|
"/docs/tutorials/payments/send-a-timed-escrow/",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Sending MPTs in JavaScript",
|
|
||||||
body: "Send a Multi-Purpose Token (MPT) to another account with the JavaScript SDK.",
|
|
||||||
path: "/docs/tutorials/tokens/mpts/sending-mpts-in-javascript/",
|
|
||||||
icon: "javascript",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Create Trust Line and Send Currency in JavaScript",
|
|
||||||
body: "Set up trust lines and send issued currencies with the JavaScript SDK.",
|
|
||||||
path: "/docs/tutorials/payments/create-trust-line-send-currency-in-javascript/",
|
|
||||||
icon: "javascript",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Create Trust Line and Send Currency in Python",
|
|
||||||
body: "Set up trust lines and send issued currencies with the Python SDK.",
|
|
||||||
path: "/docs/tutorials/payments/create-trust-line-send-currency-in-python/",
|
|
||||||
icon: "python",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Send a Conditional Escrow",
|
|
||||||
body: "Send an escrow that can be released when a specific crypto-condition is fulfilled.",
|
|
||||||
path: "/docs/tutorials/payments/send-a-conditional-escrow/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Send a Timed Escrow",
|
|
||||||
body: "Send an escrow whose only condition for release is that a specific time has passed.",
|
|
||||||
path: "/docs/tutorials/payments/send-a-timed-escrow/",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
"defi": {
|
||||||
id: "defi",
|
|
||||||
title: "DeFi",
|
|
||||||
description: "Trade, provide liquidity, and lend using native XRP Ledger DeFi features.",
|
description: "Trade, provide liquidity, and lend using native XRP Ledger DeFi features.",
|
||||||
tutorials: [
|
pinned: [
|
||||||
{
|
"/docs/tutorials/defi/dex/create-an-automated-market-maker/",
|
||||||
title: "Create an Automated Market Maker",
|
"/docs/tutorials/defi/dex/trade-in-the-decentralized-exchange/",
|
||||||
body: "Set up an AMM for a token pair and provide liquidity.",
|
"/docs/tutorials/defi/lending/use-the-lending-protocol/create-a-loan/",
|
||||||
path: "/docs/tutorials/defi/dex/create-an-automated-market-maker/",
|
"/docs/tutorials/defi/lending/use-single-asset-vaults/create-a-single-asset-vault/",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Trade in the Decentralized Exchange",
|
|
||||||
body: "Buy and sell tokens in the Decentralized Exchange (DEX).",
|
|
||||||
path: "/docs/tutorials/defi/dex/trade-in-the-decentralized-exchange/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Create a Loan Broker",
|
|
||||||
body: "Set up a loan broker to create and manage loans.",
|
|
||||||
path: "/docs/tutorials/defi/lending/use-the-lending-protocol/create-a-loan-broker/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Create a Loan",
|
|
||||||
body: "Create a loan on the XRP Ledger.",
|
|
||||||
path: "/docs/tutorials/defi/lending/use-the-lending-protocol/create-a-loan/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Create a Single Asset Vault",
|
|
||||||
body: "Create a single asset vault on the XRP Ledger.",
|
|
||||||
path: "/docs/tutorials/defi/lending/use-single-asset-vaults/create-a-single-asset-vault/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Deposit into a Vault",
|
|
||||||
body: "Deposit assets into a vault and receive shares.",
|
|
||||||
path: "/docs/tutorials/defi/lending/use-single-asset-vaults/deposit-into-a-vault/",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
"best-practices": {
|
||||||
id: "best-practices",
|
|
||||||
title: "Best Practices",
|
|
||||||
description: "Learn recommended patterns for building reliable, secure applications on the XRP Ledger.",
|
description: "Learn recommended patterns for building reliable, secure applications on the XRP Ledger.",
|
||||||
tutorials: [
|
pinned: [
|
||||||
{
|
"/docs/tutorials/best-practices/api-usage/",
|
||||||
title: "API Usage",
|
|
||||||
body: "Best practices for using XRP Ledger APIs.",
|
|
||||||
path: "/docs/tutorials/best-practices/api-usage/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Use Tickets",
|
|
||||||
body: "Use tickets to send transactions out of the normal order.",
|
|
||||||
path: "/docs/tutorials/best-practices/transaction-sending/use-tickets/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Send a Single Account Batch Transaction",
|
|
||||||
body: "Group multiple transactions together and execute them as a single atomic operation.",
|
|
||||||
path: "/docs/tutorials/best-practices/transaction-sending/send-a-single-account-batch-transaction/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Assign a Regular Key Pair",
|
|
||||||
body: "Assign a regular key pair for signing transactions.",
|
|
||||||
path: "/docs/tutorials/best-practices/key-management/assign-a-regular-key-pair/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Set Up Multi-Signing",
|
|
||||||
body: "Configure multi-signing for enhanced security.",
|
|
||||||
path: "/docs/tutorials/best-practices/key-management/set-up-multi-signing/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Send a Multi-Signed Transaction",
|
|
||||||
body: "Send a transaction with multiple signatures.",
|
|
||||||
path: "/docs/tutorials/best-practices/key-management/send-a-multi-signed-transaction/",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
"compliance-features": {
|
||||||
id: "sample-apps",
|
title: "Compliance",
|
||||||
title: "Sample Apps",
|
description: "Implement compliance controls like destination tags, credentials, and permissioned domains.",
|
||||||
|
},
|
||||||
|
"programmability": {
|
||||||
|
description: "Set up cross-chain bridges and submit interoperability transactions.",
|
||||||
|
},
|
||||||
|
"advanced-developer-topics": {
|
||||||
|
description: "Explore advanced topics like WebSocket monitoring and testing Devnet features.",
|
||||||
|
},
|
||||||
|
"sample-apps": {
|
||||||
description: "Build complete, end-to-end applications like wallets and credential services.",
|
description: "Build complete, end-to-end applications like wallets and credential services.",
|
||||||
tutorials: [
|
pinned: [
|
||||||
{
|
{
|
||||||
title: "Build a Browser Wallet in JavaScript",
|
title: "XRPL Lending Protocol Demo",
|
||||||
body: "Build a browser wallet for the XRP Ledger using JavaScript and various libraries.",
|
description: "A full-stack web application that demonstrates the end-to-end flow of the Lending Protocol and Single Asset Vaults.",
|
||||||
path: "/docs/tutorials/sample-apps/build-a-browser-wallet-in-javascript/",
|
author: { name: "Aaditya-T", url: "https://github.com/Aaditya-T" },
|
||||||
icon: "javascript",
|
github: "https://github.com/Aaditya-T/lending_test",
|
||||||
},
|
url: "https://lending-test-lovat.vercel.app/",
|
||||||
{
|
|
||||||
title: "Build a Desktop Wallet in JavaScript",
|
|
||||||
body: "Build a desktop wallet for the XRP Ledger using JavaScript, the Electron Framework, and various libraries.",
|
|
||||||
path: "/docs/tutorials/sample-apps/build-a-desktop-wallet-in-javascript/",
|
|
||||||
icon: "javascript",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Build a Desktop Wallet in Python",
|
|
||||||
body: "Build a desktop wallet for the XRP Ledger using Python and various libraries.",
|
|
||||||
path: "/docs/tutorials/sample-apps/build-a-desktop-wallet-in-python/",
|
|
||||||
icon: "python",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Credential Issuing Service in JavaScript",
|
|
||||||
body: "Build a credential issuing service using the JavaScript SDK.",
|
|
||||||
path: "/docs/tutorials/sample-apps/credential-issuing-service-in-javascript/",
|
|
||||||
icon: "javascript",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Credential Issuing Service in Python",
|
|
||||||
body: "Build a credential issuing service using the Python SDK.",
|
|
||||||
path: "/docs/tutorials/sample-apps/credential-issuing-service-in-python/",
|
|
||||||
icon: "python",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
}
|
||||||
|
|
||||||
|
// ── Components ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function TutorialCard({
|
function TutorialCard({
|
||||||
tutorial,
|
tutorial,
|
||||||
@@ -267,12 +162,10 @@ function TutorialCard({
|
|||||||
showFooter?: boolean
|
showFooter?: boolean
|
||||||
translate: (text: string) => string
|
translate: (text: string) => string
|
||||||
}) {
|
}) {
|
||||||
// Get icons: manual icon takes priority, then auto-detected languages, then XRPL fallback
|
// Get icons from auto-detected languages, or fallback to XRPL icon.
|
||||||
const icons = tutorial.icon && langIcons[tutorial.icon]
|
const icons = detectedLanguages && detectedLanguages.length > 0
|
||||||
? [langIcons[tutorial.icon]]
|
? detectedLanguages.map((lang) => langIcons[lang]).filter(Boolean)
|
||||||
: detectedLanguages && detectedLanguages.length > 0
|
: [langIcons.xrpl]
|
||||||
? detectedLanguages.map((lang) => langIcons[lang]).filter(Boolean)
|
|
||||||
: [langIcons.xrpl]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={tutorial.path} className="card">
|
<Link to={tutorial.path} className="card">
|
||||||
@@ -285,13 +178,220 @@ function TutorialCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h4 className="card-title h5">{translate(tutorial.title)}</h4>
|
<h4 className="card-title h5">{translate(tutorial.title)}</h4>
|
||||||
{tutorial.body && <p className="card-text">{translate(tutorial.body)}</p>}
|
{tutorial.description && <p className="card-text">{translate(tutorial.description)}</p>}
|
||||||
</div>
|
</div>
|
||||||
{showFooter && <div className="card-footer"></div>}
|
{showFooter && <div className="card-footer"></div>}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline meta link used in ContributionCard
|
||||||
|
function MetaLink({ href, icon, label }: {
|
||||||
|
href: string
|
||||||
|
icon: string
|
||||||
|
label: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<a href={href} target="_blank" rel="noopener noreferrer" className="meta-link">
|
||||||
|
<i className={`fa fa-${icon}`} aria-hidden="true" />
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Community Contribution Card
|
||||||
|
function ContributionCard({
|
||||||
|
tutorial,
|
||||||
|
translate,
|
||||||
|
}: {
|
||||||
|
tutorial: Tutorial
|
||||||
|
translate: (text: string) => string
|
||||||
|
}) {
|
||||||
|
const primaryUrl = tutorial.externalUrl || tutorial.github!
|
||||||
|
|
||||||
|
const handleCardClick = (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
|
if ((e.target as HTMLElement).closest(".card-meta-row")) return
|
||||||
|
window.open(primaryUrl, "_blank", "noopener,noreferrer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="card contribution-card"
|
||||||
|
onClick={handleCardClick}
|
||||||
|
onKeyDown={(e) => { if (e.key === "Enter") handleCardClick(e) }}
|
||||||
|
role="link"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div className="card-header contribution-header">
|
||||||
|
<span className="circled-logo contribution-icon">
|
||||||
|
<i className="fa fa-users" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<div className="card-meta-row">
|
||||||
|
{tutorial.author && (
|
||||||
|
<>
|
||||||
|
<MetaLink href={tutorial.author.url} icon="user" label={tutorial.author.name} />
|
||||||
|
<span className="meta-dot" aria-hidden="true">·</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MetaLink href={tutorial.github!} icon="github" label={translate("GitHub")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<h4 className="card-title h5">
|
||||||
|
{translate(tutorial.title)}
|
||||||
|
<span className="card-external-icon" aria-label={translate("External link")}>
|
||||||
|
<i className="fa fa-external-link" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
{tutorial.description && <p className="card-text">{translate(tutorial.description)}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reusable section block for rendering tutorial sections
|
||||||
|
function TutorialSectionBlock({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tutorials,
|
||||||
|
tutorialLanguages,
|
||||||
|
showFooter = false,
|
||||||
|
maxTutorials,
|
||||||
|
className = "",
|
||||||
|
translate,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
tutorials: Tutorial[]
|
||||||
|
tutorialLanguages: TutorialLanguagesMap
|
||||||
|
showFooter?: boolean
|
||||||
|
maxTutorials?: number
|
||||||
|
className?: string
|
||||||
|
translate: (text: string) => string
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const sectionRef = useRef<HTMLElement>(null)
|
||||||
|
const hasMore = maxTutorials ? tutorials.length > maxTutorials : false
|
||||||
|
const displayTutorials = maxTutorials && !expanded ? tutorials.slice(0, maxTutorials) : tutorials
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (expanded && sectionRef.current) {
|
||||||
|
const offsetTop = sectionRef.current.getBoundingClientRect().top + window.scrollY
|
||||||
|
setExpanded(false)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.scrollTo({ top: offsetTop - 20 })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setExpanded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={sectionRef} className={`container-new pt-10 pb-14 ${className}`.trim()} id={id}>
|
||||||
|
<div className="col-12 col-xl-8 p-0">
|
||||||
|
<h3 className="h4 mb-3">{translate(title)}</h3>
|
||||||
|
<p className="mb-4">{translate(description)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="row tutorial-cards">
|
||||||
|
{displayTutorials.map((tutorial) => (
|
||||||
|
<div key={tutorial.path} className="col-lg-4 col-md-6 mb-5">
|
||||||
|
{tutorial.github ? (
|
||||||
|
<ContributionCard tutorial={tutorial} translate={translate} />
|
||||||
|
) : (
|
||||||
|
<TutorialCard
|
||||||
|
tutorial={tutorial}
|
||||||
|
detectedLanguages={tutorialLanguages[tutorial.path]}
|
||||||
|
showFooter={showFooter}
|
||||||
|
translate={translate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{hasMore && (
|
||||||
|
<div className="explore-more-wrapper">
|
||||||
|
<button
|
||||||
|
className="explore-more-link"
|
||||||
|
onClick={handleToggle}
|
||||||
|
>
|
||||||
|
{expanded ? translate("Show less") : translate("Explore more")} {expanded ? "↑" : "→"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copyable URL component with click-to-copy functionality
|
||||||
|
function CopyableUrl({ url, translate }: { url: string; translate: (text: string) => string }) {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`quick-ref-value-btn ${copied ? "copied" : ""}`}
|
||||||
|
onClick={handleCopy}
|
||||||
|
title={copied ? translate("Copied!") : translate("Click to copy")}
|
||||||
|
>
|
||||||
|
<code className="quick-ref-value">{url}</code>
|
||||||
|
<span className="copy-icon">{copied ? "✓" : ""}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick reference card showing public server URLs and faucet link
|
||||||
|
function QuickReferenceCard({ translate }: { translate: (text: string) => string }) {
|
||||||
|
return (
|
||||||
|
<div className="quick-ref-card">
|
||||||
|
<div className="quick-ref-section">
|
||||||
|
<span className="quick-ref-label">{translate("PUBLIC SERVERS")}</span>
|
||||||
|
<div className="quick-ref-group">
|
||||||
|
<span className="quick-ref-key"><strong>{translate("Mainnet")}</strong></span>
|
||||||
|
<div className="quick-ref-urls">
|
||||||
|
<span className="quick-ref-protocol">{translate("WebSocket")}</span>
|
||||||
|
<CopyableUrl url="wss://xrplcluster.com" translate={translate} />
|
||||||
|
<span className="quick-ref-protocol">{translate("JSON-RPC")}</span>
|
||||||
|
<CopyableUrl url="https://xrplcluster.com" translate={translate} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="quick-ref-group">
|
||||||
|
<span className="quick-ref-key"><strong>{translate("Testnet")}</strong></span>
|
||||||
|
<div className="quick-ref-urls">
|
||||||
|
<span className="quick-ref-protocol">{translate("WebSocket")}</span>
|
||||||
|
<CopyableUrl url="wss://s.altnet.rippletest.net:51233" translate={translate} />
|
||||||
|
<span className="quick-ref-protocol">{translate("JSON-RPC")}</span>
|
||||||
|
<CopyableUrl url="https://s.altnet.rippletest.net:51234" translate={translate} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link to="/docs/tutorials/public-servers/" className="quick-ref-link">
|
||||||
|
{translate("View all servers")} →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="quick-ref-divider"></div>
|
||||||
|
<div className="quick-ref-section">
|
||||||
|
<Link to="/resources/dev-tools/xrp-faucets/" className="quick-ref-faucet">
|
||||||
|
<span>{translate("Get Test XRP")}</span>
|
||||||
|
<span className="quick-ref-arrow">→</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page Component ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function TutorialsIndex() {
|
export default function TutorialsIndex() {
|
||||||
const { useTranslate, usePageSharedData } = useThemeHooks()
|
const { useTranslate, usePageSharedData } = useThemeHooks()
|
||||||
const { translate } = useTranslate()
|
const { translate } = useTranslate()
|
||||||
@@ -299,65 +399,160 @@ export default function TutorialsIndex() {
|
|||||||
// Get auto-detected languages from the plugin (maps tutorial paths to language arrays).
|
// Get auto-detected languages from the plugin (maps tutorial paths to language arrays).
|
||||||
const tutorialLanguages = usePageSharedData<TutorialLanguagesMap>("tutorial-languages") || {}
|
const tutorialLanguages = usePageSharedData<TutorialLanguagesMap>("tutorial-languages") || {}
|
||||||
|
|
||||||
|
// Get tutorial metadata and sidebar categories from the tutorial-metadata plugin.
|
||||||
|
const tutorialMetadata = usePageSharedData<{
|
||||||
|
tutorials: TutorialMetadataItem[]
|
||||||
|
categories: { id: string; title: string }[]
|
||||||
|
}>("tutorial-metadata")
|
||||||
|
const allTutorials = tutorialMetadata?.tutorials || []
|
||||||
|
const sidebarCategories = tutorialMetadata?.categories || []
|
||||||
|
|
||||||
|
// What's New: most recently modified tutorials, excluding Get Started.
|
||||||
|
const whatsNewConfig = sectionConfig["whats-new"]!
|
||||||
|
const getStartedPaths = new Set(
|
||||||
|
(sectionConfig["get-started"]?.pinned || []).map(getPinnedPath)
|
||||||
|
)
|
||||||
|
const whatsNewTutorials: Tutorial[] = allTutorials
|
||||||
|
.filter((tutorial) => !getStartedPaths.has(tutorial.path))
|
||||||
|
.slice(0, MAX_WHATS_NEW)
|
||||||
|
.map((tutorial) => toTutorial(tutorial))
|
||||||
|
|
||||||
|
// Category sections (including Get Started): ordered by sectionConfig, then any new sidebar categories.
|
||||||
|
const sections = buildCategorySections(sidebarCategories, allTutorials)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="landing page-tutorials landing-builtin-bg">
|
<main className="landing page-tutorials landing-builtin-bg">
|
||||||
<section className="container-new py-26">
|
{/* Hero Section */}
|
||||||
<div className="col-lg-8 mx-auto text-lg-center">
|
<section className="container-new py-20">
|
||||||
<div className="d-flex flex-column-reverse">
|
<div className="row align-items-center">
|
||||||
<h1 className="mb-0">
|
<div className="col-lg-7">
|
||||||
{translate("Crypto Wallet and Blockchain Development Tutorials")}
|
<div className="d-flex flex-column-reverse">
|
||||||
</h1>
|
<h1 className="mb-0">
|
||||||
<h6 className="eyebrow mb-3">{translate("Tutorials")}</h6>
|
{translate("Crypto Wallet and Blockchain Development Tutorials")}
|
||||||
</div>
|
</h1>
|
||||||
{/* Table of Contents */}
|
<h6 className="eyebrow mb-3">{translate("Tutorials")}</h6>
|
||||||
<nav className="mt-4">
|
|
||||||
<ul className="page-toc no-sideline d-flex flex-wrap justify-content-center gap-2 mb-0">
|
|
||||||
<li><a href="#get-started">{translate("Get Started with SDKs")}</a></li>
|
|
||||||
{sections.map((section) => (
|
|
||||||
<li key={section.id}><a href={`#${section.id}`}>{translate(section.title)}</a></li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Get Started */}
|
|
||||||
<section className="container-new pt-10 pb-20" id="get-started">
|
|
||||||
<div className="col-12 col-xl-8 p-0">
|
|
||||||
<h3 className="h4 mb-3">{translate("Get Started with SDKs")}</h3>
|
|
||||||
<p className="mb-4">
|
|
||||||
{translate("These tutorials walk you through the basics of building a very simple XRP Ledger-connected application using your favorite programming language.")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="row tutorial-cards">
|
|
||||||
{getStartedTutorials.map((tutorial, idx) => (
|
|
||||||
<div key={idx} className="col-lg-4 col-md-6 mb-5">
|
|
||||||
<TutorialCard tutorial={tutorial} showFooter translate={translate} />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<nav className="mt-4">
|
||||||
|
<ul className="page-toc no-sideline d-flex flex-wrap gap-2 mb-0">
|
||||||
|
{whatsNewTutorials.length > 0 && (
|
||||||
|
<li><Link to="#whats-new">{translate(whatsNewConfig.title)}</Link></li>
|
||||||
|
)}
|
||||||
|
{sections.map((section) => (
|
||||||
|
<li key={section.id}><Link to={`#${section.id}`}>{translate(section.title)}</Link></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-5 mt-6 mt-lg-0">
|
||||||
|
<QuickReferenceCard translate={translate} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Other Tutorials */}
|
{/* What's New */}
|
||||||
|
{whatsNewTutorials.length > 0 && (
|
||||||
|
<TutorialSectionBlock
|
||||||
|
id="whats-new"
|
||||||
|
title={whatsNewConfig.title!}
|
||||||
|
description={whatsNewConfig.description!}
|
||||||
|
tutorials={whatsNewTutorials}
|
||||||
|
tutorialLanguages={tutorialLanguages}
|
||||||
|
showFooter
|
||||||
|
className="whats-new-section pb-20"
|
||||||
|
translate={translate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tutorial Sections */}
|
||||||
{sections.map((section) => (
|
{sections.map((section) => (
|
||||||
<section className="container-new pt-10 pb-10" key={section.id} id={section.id}>
|
<TutorialSectionBlock
|
||||||
<div className="col-12 col-xl-8 p-0">
|
key={section.id}
|
||||||
<h3 className="h4 mb-3">{translate(section.title)}</h3>
|
id={section.id}
|
||||||
<p className="mb-4">{translate(section.description)}</p>
|
title={section.title}
|
||||||
</div>
|
description={section.description}
|
||||||
<div className="row tutorial-cards">
|
tutorials={section.tutorials}
|
||||||
{section.tutorials.slice(0, 6).map((tutorial, idx) => (
|
tutorialLanguages={tutorialLanguages}
|
||||||
<div key={idx} className="col-lg-4 col-md-6 mb-5">
|
maxTutorials={section.showFooter ? undefined : MAX_TUTORIALS_PER_SECTION}
|
||||||
<TutorialCard
|
showFooter={section.showFooter}
|
||||||
tutorial={tutorial}
|
className={section.showFooter ? "pb-20" : "category-section"}
|
||||||
detectedLanguages={tutorialLanguages[tutorial.path]}
|
translate={translate}
|
||||||
translate={translate}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Type guard for external community contributions */
|
||||||
|
function isExternalContribution(entry: PinnedTutorial): entry is PinnedExternalTutorial {
|
||||||
|
return typeof entry !== "string" && "github" in entry
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get path from pinned tutorial entry*/
|
||||||
|
function getPinnedPath(entry: PinnedTutorial): string {
|
||||||
|
return typeof entry === "string" ? entry : isExternalContribution(entry) ? entry.github : entry.path
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert tutorial metadata to the common Tutorial type */
|
||||||
|
function toTutorial(t: TutorialMetadataItem, descriptionOverride?: string): Tutorial {
|
||||||
|
return {
|
||||||
|
title: t.title,
|
||||||
|
description: descriptionOverride || t.description,
|
||||||
|
path: t.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build Tutorial objects from pinned entries, resolving metadata for internal paths */
|
||||||
|
function buildPinnedTutorials(entries: PinnedTutorial[], allTutorials: TutorialMetadataItem[]): Tutorial[] {
|
||||||
|
return entries
|
||||||
|
.map((entry): Tutorial | null => {
|
||||||
|
if (isExternalContribution(entry)) {
|
||||||
|
return {
|
||||||
|
title: entry.title,
|
||||||
|
description: entry.description,
|
||||||
|
path: entry.url || entry.github,
|
||||||
|
author: entry.author,
|
||||||
|
github: entry.github,
|
||||||
|
externalUrl: entry.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const path = getPinnedPath(entry)
|
||||||
|
const descOverride = typeof entry === "string" ? undefined : entry.description
|
||||||
|
const metadata = allTutorials.find((t) => t.path === path)
|
||||||
|
return metadata ? toTutorial(metadata, descOverride) : null
|
||||||
|
})
|
||||||
|
.filter((t): t is Tutorial => t !== null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build category sections ordered by sectionConfig, with new sidebar categories appended */
|
||||||
|
function buildCategorySections(
|
||||||
|
sidebarCategories: { id: string; title: string }[],
|
||||||
|
allTutorials: TutorialMetadataItem[],
|
||||||
|
): TutorialSection[] {
|
||||||
|
const specialIds = new Set(["whats-new"])
|
||||||
|
const sidebarMap = new Map(sidebarCategories.map((category) => [category.id, category]))
|
||||||
|
const allPinnedPaths = new Set(
|
||||||
|
Object.values(sectionConfig).flatMap((config) => (config.pinned || []).map(getPinnedPath))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sections follow sectionConfig key order. New sidebar categories not in sectionConfig are appended at the end.
|
||||||
|
const configIds = Object.keys(sectionConfig).filter((id) => !specialIds.has(id))
|
||||||
|
const newIds = sidebarCategories
|
||||||
|
.filter((category) => !specialIds.has(category.id) && !sectionConfig[category.id])
|
||||||
|
.map((category) => category.id)
|
||||||
|
|
||||||
|
return [...configIds, ...newIds]
|
||||||
|
.filter((id) => sidebarMap.has(id))
|
||||||
|
.map((id) => {
|
||||||
|
const config = sectionConfig[id]
|
||||||
|
const title = config?.title || sidebarMap.get(id)!.title
|
||||||
|
const description = config?.description || ""
|
||||||
|
const pinned = buildPinnedTutorials(config?.pinned || [], allTutorials)
|
||||||
|
const remaining = allTutorials
|
||||||
|
.filter((t) => t.category === id && !allPinnedPaths.has(t.path))
|
||||||
|
.map((t) => toTutorial(t))
|
||||||
|
return { id, title, description, tutorials: [...pinned, ...remaining], showFooter: config?.showFooter }
|
||||||
|
})
|
||||||
|
.filter((section) => section.tutorials.length > 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -286,7 +286,8 @@
|
|||||||
"asset2": {
|
"asset2": {
|
||||||
"currency": "TST",
|
"currency": "TST",
|
||||||
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
|
||||||
}
|
},
|
||||||
|
"ledger_index": "validated"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -320,128 +320,3 @@ main article .card-grid {
|
|||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tutorial cards */
|
|
||||||
.page-tutorials .tutorial-cards {
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 280px;
|
|
||||||
transition: all 0.35s ease-out;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
border-bottom: none;
|
|
||||||
background: transparent;
|
|
||||||
padding: 1.5rem 1.5rem 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circled-logo {
|
|
||||||
margin-left: -10px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-text {
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
background-color: transparent;
|
|
||||||
border-top: none;
|
|
||||||
padding: 0;
|
|
||||||
height: 40px;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: bottom;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply colored footers to each card
|
|
||||||
> div:nth-child(1) .card .card-footer {
|
|
||||||
background-image: url("../img/cards/3-col-pink.svg");
|
|
||||||
}
|
|
||||||
> div:nth-child(2) .card .card-footer {
|
|
||||||
background-image: url("../img/cards/3col-blue-light-blue.svg");
|
|
||||||
}
|
|
||||||
> div:nth-child(3) .card .card-footer {
|
|
||||||
background-image: url("../img/cards/3-col-light-blue.svg");
|
|
||||||
}
|
|
||||||
> div:nth-child(4) .card .card-footer {
|
|
||||||
background-image: url("../img/cards/3col-blue-green.svg");
|
|
||||||
}
|
|
||||||
> div:nth-child(5) .card .card-footer {
|
|
||||||
background-image: url("../img/cards/3col-magenta.svg");
|
|
||||||
}
|
|
||||||
> div:nth-child(6) .card .card-footer {
|
|
||||||
background-image: url("../img/cards/3-col-orange.svg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .page-tutorials .tutorial-cards .circled-logo img[alt="HTTP / WebSocket"] {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOC buttons for tutorials page
|
|
||||||
.page-tutorials .page-toc.no-sideline {
|
|
||||||
border-left: none;
|
|
||||||
gap: 0.75rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
a {
|
|
||||||
border-radius: 100px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin: 0;
|
|
||||||
background-color: $gray-800;
|
|
||||||
color: $gray-200;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
background-color: $blue-purple-500;
|
|
||||||
color: $white;
|
|
||||||
border-left: none;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .page-tutorials .page-toc.no-sideline {
|
|
||||||
li a {
|
|
||||||
background-color: $gray-200;
|
|
||||||
color: $gray-800;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
background-color: $blue-purple-500;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
489
styles/_tutorials.scss
Normal file
489
styles/_tutorials.scss
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
// Tutorials landing page styles
|
||||||
|
|
||||||
|
// Card footer gradient images
|
||||||
|
$card-footers: (
|
||||||
|
"3-col-pink",
|
||||||
|
"3col-blue-light-blue",
|
||||||
|
"3-col-light-blue",
|
||||||
|
"3col-blue-green",
|
||||||
|
"3col-magenta",
|
||||||
|
"3-col-orange"
|
||||||
|
);
|
||||||
|
|
||||||
|
$whats-new-footers: (
|
||||||
|
"3col-green-purple",
|
||||||
|
"3col-purple-blue-green",
|
||||||
|
"3col-green-blue"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tutorial cards
|
||||||
|
.page-tutorials .tutorial-cards {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
transition: all 0.35s ease-out;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-bottom: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 1.5rem 1.5rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circled-logo {
|
||||||
|
margin-left: -10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
background-color: transparent;
|
||||||
|
border-top: none;
|
||||||
|
padding: 0;
|
||||||
|
height: 40px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: bottom;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply colored footers to each card
|
||||||
|
@for $i from 1 through length($card-footers) {
|
||||||
|
> div:nth-child(#{$i}) .card .card-footer {
|
||||||
|
background-image: url("../img/cards/#{nth($card-footers, $i)}.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light mode: invert HTTP/WebSocket icon
|
||||||
|
.light .page-tutorials .tutorial-cards .circled-logo img[alt="HTTP / WebSocket"] {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contribution Card - community contribution with meta links
|
||||||
|
.page-tutorials .tutorial-cards .contribution-card {
|
||||||
|
.contribution-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contribution-icon {
|
||||||
|
background: rgba($blue-purple-500, 0.15);
|
||||||
|
color: $blue-purple-300;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-external-icon {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
color: $gray-500;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: -4px;
|
||||||
|
|
||||||
|
.meta-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: $gray-300;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $blue-purple-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-dot {
|
||||||
|
color: $gray-500;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light mode: Contribution Card
|
||||||
|
.light .page-tutorials .tutorial-cards .contribution-card {
|
||||||
|
.contribution-icon {
|
||||||
|
background: rgba($blue-purple-500, 0.1);
|
||||||
|
color: $blue-purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta-row {
|
||||||
|
.meta-link {
|
||||||
|
color: $gray-600;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $blue-purple-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-dot {
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tutorial category section spacing
|
||||||
|
.page-tutorials .category-section + .category-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explore more link
|
||||||
|
.page-tutorials .explore-more-wrapper {
|
||||||
|
margin-top: -1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
.explore-more-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: $blue-purple-300;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .page-tutorials .explore-more-wrapper .explore-more-link {
|
||||||
|
color: $blue-purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOC navigation buttons
|
||||||
|
.page-tutorials .page-toc.no-sideline {
|
||||||
|
border-left: none;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
background-color: $gray-800;
|
||||||
|
color: $gray-200;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background-color: $blue-purple-500;
|
||||||
|
color: $white;
|
||||||
|
border-left: none;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light mode: TOC buttons
|
||||||
|
.light .page-tutorials .page-toc.no-sideline {
|
||||||
|
li a {
|
||||||
|
background-color: $gray-200;
|
||||||
|
color: $gray-800;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background-color: $blue-purple-500;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What's New section
|
||||||
|
.whats-new-section {
|
||||||
|
// Gradient underline on section title
|
||||||
|
h3 {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, $blue-purple-500, $green-400);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different footer colors for What's New cards
|
||||||
|
.tutorial-cards {
|
||||||
|
@for $i from 1 through length($whats-new-footers) {
|
||||||
|
> div:nth-child(#{$i}) .card .card-footer {
|
||||||
|
background-image: url("../img/cards/#{nth($whats-new-footers, $i)}.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Reference Card
|
||||||
|
.page-tutorials .quick-ref-card {
|
||||||
|
background: rgba($gray-800, 0.7);
|
||||||
|
border: 1px solid $gray-700;
|
||||||
|
border-left: 3px solid $blue-purple-500;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
margin-left: auto;
|
||||||
|
max-width: 480px;
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
margin-left: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-section {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: $blue-purple-300;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-group {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-key {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: $gray-300;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-urls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
gap: 0.25rem 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-protocol {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $gray-500;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-value {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: $blue-purple-300;
|
||||||
|
background: rgba($gray-900, 0.5);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-value-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
.quick-ref-value {
|
||||||
|
background: rgba($gray-800, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied .quick-ref-value {
|
||||||
|
background: rgba($green-500, 0.2);
|
||||||
|
color: $green-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: $green-400;
|
||||||
|
min-width: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-link {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $blue-purple-300;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: $gray-700;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-faucet {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $blue-purple-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-arrow {
|
||||||
|
color: $blue-purple-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light mode: Quick Reference Card
|
||||||
|
.light .page-tutorials .quick-ref-card {
|
||||||
|
background: rgba($white, 0.95);
|
||||||
|
border-color: $gray-300;
|
||||||
|
border-left: 3px solid $blue-purple-500;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.quick-ref-label {
|
||||||
|
color: $blue-purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-key {
|
||||||
|
color: $gray-700;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-protocol {
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-value {
|
||||||
|
color: $blue-purple-600;
|
||||||
|
background: rgba($gray-300, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-value-btn {
|
||||||
|
&:hover .quick-ref-value {
|
||||||
|
background: rgba($gray-400, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied .quick-ref-value {
|
||||||
|
background: rgba($green-500, 0.15);
|
||||||
|
color: $green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
color: $green-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-link {
|
||||||
|
color: $blue-purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-divider {
|
||||||
|
background: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-faucet {
|
||||||
|
color: $gray-900;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $blue-purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-ref-arrow {
|
||||||
|
color: $blue-purple-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ $line-height-base: 1.5;
|
|||||||
@import "_pages.scss";
|
@import "_pages.scss";
|
||||||
@import "_rpc-tool.scss";
|
@import "_rpc-tool.scss";
|
||||||
@import "_blog.scss";
|
@import "_blog.scss";
|
||||||
|
@import "_tutorials.scss";
|
||||||
@import "_feedback.scss";
|
@import "_feedback.scss";
|
||||||
@import "_video.scss";
|
@import "_video.scss";
|
||||||
@import "_contribute.scss";
|
@import "_contribute.scss";
|
||||||
|
|||||||
Reference in New Issue
Block a user