diff --git a/docs/about/faq.mdx b/docs/about/faq.mdx
index e28e6a81..1c307293 100644
--- a/docs/about/faq.mdx
+++ b/docs/about/faq.mdx
@@ -5,39 +5,51 @@ slug: /faq
sidebar_position: 3
---
-## Overview
+import { FaqAccordion } from "/src/components/FaqAccordion";
-### Where can I get additional help, ask questions, or bond with the Meshtastic community?
-
-This site (which has a great search function) is the preferred place for up-to-date documentation. Many of our users and developers hang out on the [Meshtastic Discord](https://discord.gg/ktMAKGBnBs) server where you may connect with like-minded people.
-
-### How can I contribute to Meshtastic?
-
-Everyone contributes in a different way. Join the [Meshtastic Discord](https://discord.gg/ktMAKGBnBs) and introduce yourself. We're all very friendly. If you'd like to pitch in some code, check out the [Development](/docs/developers) menu on the left.
-
-
-
-## Android Client
-
-### What versions of Android does the Meshtastic Android App require?
-
-Minimum requirement is Android 5 (Lollipop 2014, first BLE support), however at least Android 6 (Marshmallow 2015) is recommended as Bluetooth is more stable. While Android 5/6 are officially supported by Meshtastic, it is _not_ recommended that you purchase devices with these versions due to their limited OS support and limited battery life due to age. Many newer models exist that are very affordable. A good resource to use when researching affordable devices is the [LineageOS Supported Devices List](https://wiki.lineageos.org/devices/).
-
-### What does the icon next to the message mean?
+export const GeneralFaq = [
+ {
+ title: "Where can I get additional help, ask questions, or bond with the Meshtastic community?",
+ content: `This site (which has a great search function) is the preferred place for up-to-date documentation. Many of our users and developers hang out on the [Meshtastic Discord](https://discord.gg/ktMAKGBnBs) server where you may connect with like-minded people.`,
+ },
+ {
+ title: "How can I contribute to Meshtastic?",
+ content: "Everyone contributes in a different way. Join the [Meshtastic Discord](https://discord.gg/ktMAKGBnBs) and introduce yourself. We're all very friendly. If you'd like to pitch in some code, check out the [Development](/docs/developers) menu on the left.",
+ },
+];
+export const AndroidFaq = [
+ {
+ title: "What versions of Android does the Meshtastic Android App require?",
+ content: `Minimum requirement is Android 5 (Lollipop 2014, first BLE support), however at least Android 6 (Marshmallow 2015) is recommended as Bluetooth is more stable. While Android 5/6 are officially supported by Meshtastic, it is _not_ recommended that you purchase devices with these versions due to their limited OS support and limited battery life due to age. Many newer models exist that are very affordable. A good resource to use when researching affordable devices is the [LineageOS Supported Devices List](https://wiki.lineageos.org/devices/).`,
+ },
+ {
+ title: "What does the icon next to the message mean?",
+ content: `
- Cloud with an up arrow - Queued on the app to be sent to your device.
- Cloud only - Queued on the device to be sent over the mesh.
- Cloud with a check mark - At least one other node on the mesh acknowledged the message.
- Person with a check mark - The intended recipient of your direct message acknowledged the message.
-- Cloud crossed out - Not acknowledged or message error.
+- Cloud crossed out - Not acknowledged or message error.`,
+ },
+ {
+ title: "How can I clear the message history?",
+ content: `Long press any message to select and show the menu with "delete" and "select all" buttons.`,
+ },
+ {
+ title: "After a fresh firmware install, my node is not connecting via Bluetooth. What should I do?",
+ content: `Try forgetting the Bluetooth connection from the Android Bluetooth Settings menu. Re-pair and try again. This is a security measure and there is no workaround for it. It prevents apps and other accessories from spoofing an existing accessory by un-pairing and "re-pairing" themselves without the users' knowledge.`,
+ },
+];
-### How can I clear the message history?
+## Overview
-Long press any message to select and show the menu with "delete" and "select all" buttons.
+
-### After a fresh firmware install, my node is not connecting via Bluetooth. What should I do?
+
-Try forgetting the Bluetooth connection from the Android Bluetooth Settings menu. Re-pair and try again. This is a security measure and there is no workaround for it. It prevents apps and other accessories from spoofing an existing accessory by un-pairing and "re-pairing" themselves without the users' knowledge.
+## Android Client
+
diff --git a/package.json b/package.json
index 67ac5d29..9c6cd02d 100644
--- a/package.json
+++ b/package.json
@@ -27,8 +27,10 @@
"dotenv": "^16.3.1",
"postcss": "^8.4.33",
"react": "^18.2.0",
+ "react-accessible-accordion": "^5.0.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
+ "react-markdown": "^9.0.1",
"remark-deflist": "^1.0.0",
"swr": "^2.2.4",
"tailwindcss": "^3.4.1"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 56fe66f7..756fa2f6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,12 +50,18 @@ dependencies:
react:
specifier: ^18.2.0
version: 18.2.0
+ react-accessible-accordion:
+ specifier: ^5.0.0
+ version: 5.0.0(react-dom@18.2.0)(react@18.2.0)
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-icons:
specifier: ^4.12.0
version: 4.12.0(react@18.2.0)
+ react-markdown:
+ specifier: ^9.0.1
+ version: 9.0.1(@types/react@18.2.47)(react@18.2.0)
remark-deflist:
specifier: ^1.0.0
version: 1.0.0
@@ -5793,6 +5799,10 @@ packages:
engines: {node: '>=8'}
dev: false
+ /html-url-attributes@3.0.0:
+ resolution: {integrity: sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==}
+ dev: false
+
/html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
dev: false
@@ -8413,6 +8423,16 @@ packages:
strip-json-comments: 2.0.1
dev: false
+ /react-accessible-accordion@5.0.0(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-MT2obYpTgLIIfPr9d7hEyvPB5rg8uJcHpgA83JSRlEUHvzH48+8HJPvzSs+nM+XprTugDgLfhozO5qyJpBvYRQ==}
+ peerDependencies:
+ react: ^16.3.2 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.3.3 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/react-dev-utils@12.0.1(typescript@5.3.3)(webpack@5.89.0):
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
@@ -8529,6 +8549,28 @@ packages:
webpack: 5.89.0
dev: false
+ /react-markdown@9.0.1(@types/react@18.2.47)(react@18.2.0):
+ resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
+ peerDependencies:
+ '@types/react': '>=18'
+ react: '>=18'
+ dependencies:
+ '@types/hast': 3.0.3
+ '@types/react': 18.2.47
+ devlop: 1.1.0
+ hast-util-to-jsx-runtime: 2.3.0
+ html-url-attributes: 3.0.0
+ mdast-util-to-hast: 13.0.2
+ react: 18.2.0
+ remark-parse: 11.0.0
+ remark-rehype: 11.0.0
+ unified: 11.0.4
+ unist-util-visit: 5.0.0
+ vfile: 6.0.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/react-router-config@5.1.1(react-router@5.3.4)(react@18.2.0):
resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==}
peerDependencies:
diff --git a/src/components/FaqAccordion.tsx b/src/components/FaqAccordion.tsx
new file mode 100644
index 00000000..bc4f360e
--- /dev/null
+++ b/src/components/FaqAccordion.tsx
@@ -0,0 +1,99 @@
+import React from "react";
+import ReactMarkdown from "react-markdown";
+import {
+ Accordion,
+ AccordionItem,
+ AccordionItemHeading,
+ AccordionItemButton,
+ AccordionItemPanel,
+} from "react-accessible-accordion";
+import "react-accessible-accordion/dist/fancy-example.css";
+
+export interface Faq {
+ title: string;
+ content: string;
+}
+
+/**
+ * Gets the query parameter `openFaqItems` which is an array of
+ * faq items that should be pre-opened
+ * @type {Function}
+ */
+const getOpenFaqItemsFromUrl = (slug: String): string[] => {
+ // Use URLSearchParams to parse the query parameters from the current URL
+ const searchParams = new URLSearchParams(window.location.search);
+
+ // Get the 'openFaqItems' parameter as a comma-separated string
+ const openFaqItemsString = searchParams.get(`openFaqItems-${slug}`);
+
+ // If the parameter exists, split it by commas into an array; otherwise, return an empty array
+ return openFaqItemsString ? openFaqItemsString.split(',') : [];
+};
+
+export const FaqAccordion = ({
+ rows,
+ slug
+}: { rows: Faq[], slug: String }): JSX.Element => {
+ // Set the faq structured data
+ const faqStructuredData = {
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: rows.map((row) => ({
+ "@type": "Question",
+ name: row.title,
+ acceptedAnswer: {
+ "@type": "Answer",
+ text: row.content,
+ },
+ })),
+ };
+
+ // Use the getOpenFaqItemsFromUrl function to set the
+ // initial state of preExpanded based on URL parameters
+ const [preExpanded, setPreExpanded] = React.useState(getOpenFaqItemsFromUrl(slug));
+
+ /**
+ * Updates query parameters in the url when items are opened
+ * so that a link can be shared with the faq item already opened
+ */
+ const handleChange = (openFaqItems: (string | number)[]): void => {
+ // Get current url params
+ const searchParams = new URLSearchParams(window.location.search);
+
+ // Convert openFaqItems to a comma-separated string and update/add the parameter
+ searchParams.set(`openFaqItems-${slug}`, openFaqItems.map(String).join(','));
+
+ // Construct the new URL, preserve existing parameters
+ const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${searchParams.toString()}`;
+
+ // Use history.pushState to change the URL without reloading the page
+ window.history.pushState({ path: newUrl }, '', newUrl);
+ };
+
+ return (
+ <>
+
+
+ {rows.map((row, index) => (
+
+
+ {row.title}
+
+
+ {row.content}
+
+
+ ))}
+
+ >
+ );
+};