Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 95 additions & 14 deletions client/src/pages/FAQ/FAQContent.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,103 @@
export const faqData = [
export interface FAQItem {
question: string;
answer: string;
}

export interface FAQCategory {
title: string;
items: FAQItem[];
}

export const faqData: FAQData[] = [
{
id: "how-to-buy",
question: "How do I buy a ticket?",
answer: "To buy a ticket, connect your Stellar-compatible wallet (like Albedo or Rabbit), select the lottery you wish to enter, and click 'Purchase'. You will need a small amount of XLM for the transaction fee."
title: "Wallets",
items: [
{
id: "wallet-support",
question: "Which wallets are supported by the platform?",
answer: "We support major Stellar-compatible wallets, including Freighter, xBull, and Albedo. We highly recommend using Freighter for the smoothest experience on Soroban smart contract networks."
},
{
question: "How do I connect my Freighter wallet?",
answer: "Click the 'Connect Wallet' button in the top right navigation bar, select 'Freighter' from the modal options, and approve the connection request popup inside your Freighter extension window."
},
{
question: "Why is my wallet showing the wrong network?",
answer: "Ensure your wallet extension is toggled to the network matching our platform (e.g., Testnet for testing or Mainnet for live draws). You can switch networks in your wallet extension's settings panel."
}
]
},
{
id: "how-to-create",
question: "How do I create a lottery?",
answer: "Navigate to the 'Create' dashboard. Fill in the prize pool details, ticket price, and end date. Once submitted, you will sign a transaction to deploy the Soroban smart contract."
title: "Tickets & Raffles",
items: [
{
id: "how-to-buy",
question: "How do I buy a ticket?",
answer: "To buy a ticket, connect your Stellar-compatible wallet (like Albedo or Rabbit), select the lottery you wish to enter, and click 'Purchase'. You will need a small amount of XLM for the transaction fee."
},
{
question: "Can I buy multiple tickets for a single raffle?",
answer: "Yes! There is no limit on how many tickets a single wallet address can purchase, provided the raffle's max ticket pool capacity hasn't been hit. More tickets grant a proportionally higher mathematical probability of winning."
},
{
question: "What is the refund policy if a raffle is cancelled?",
answer: "If a raffle is cancelled or fails to reach its minimum threshold criteria before the expiration deadline, the Soroban smart contract enters a refundable state. You can claim a 100% refund of your ticket purchase cost directly through the user interface."
},
{
question: "Are my tickets transferable after purchase?",
answer: "No. Raffle tickets are bound directly to the minting/purchasing wallet address on-chain and cannot be traded or transferred to another account."
}
]
},
{
id: "what-is-vrf",
question: "What is VRF?",
answer: "VRF stands for Verifiable Random Function. It is a cryptographic primitive used to ensure that the winner selection process is 100% fair, transparent, and cannot be manipulated by the developers or other users."
title: "Randomness & Oracle Mechanisms",
items: [
{
question: "How is the winner chosen?",
answer: "Winners are drawn using an automated execution triggered on the Soroban smart contract. The system pulls an external entropy payload generated by a secure decentralized oracle framework to dictate the winning ticket index impartially."
},
{
question: "What is a VRF proof?",
answer: "A Verifiable Random Function (VRF) proof is a cryptographic proof that demonstrates a random number was generated purely from a secret seed and public key parameters. It guarantees the output is mathematically unpredictable, tamper-proof, and fully verifiable by anyone on-chain."
},
{
question: "Can developers or creators rig the outcome?",
answer: "No. Because the calculation runs entirely within an open-source Soroban smart contract coupled with verifiable VRF oracles, neither platform operators nor raffle creators can manipulate the generation sequence."
}
]
},
{
id: "wallet-support",
question: "Which wallets are supported?",
answer: "Currently, we support Albedo, Freighter, and any wallet compatible with the Stellar wallet-connect standard."
title: "Fees & Network Gas",
items: [
{
question: "What are the transaction fees on the platform?",
answer: "Every ticket purchase and interaction relies on the Stellar network's native fee structure. Soroban smart contracts require execution fees ('resource fees') paid in XLM. These are highly scalable and typically cost a fraction of a cent."
},
{
question: "Who pays for the oracle random number generation?",
answer: "The platform covers or subsidizes the base operational infrastructure overhead of pulling the oracle feed into the smart contract state, ensuring users only handle their standard ticket purchase interaction gas."
},
{
question: "What happens if my transaction fails due to low fees?",
answer: "If your wallet sets too low of an XLM base fee during high network utilization spikes, the transaction will time out. Simply bump your max fee parameter in Freighter and retry."
}
]
},
{
title: "Transparency & Security",
items: [
{
question: "Where can I verify the draw on-chain?",
answer: "Every single raffle deployment, ticket purchase execution, and winning draw logs an event stream to the ledger. You can verify all steps by inspecting the specific Soroban Contract ID on network block explorers like StellarExpert."
},
{
question: "Is the raffle contract open-source?",
answer: "Yes, our Soroban Rust smart contracts are completely open-source. Anyone can audit the logic, structural parameters, state machines, and cryptographic verification mechanisms on our official GitHub organization."
},
{
question: "How long after the countdown does the automated draw happen?",
answer: "Once the deadline timestamp passes, an on-chain execution call is initiated. The process relies on blockchain block confirmation times, usually triggering and resolving inside 5 to 10 seconds."
}
]
}
];
];
159 changes: 59 additions & 100 deletions client/src/pages/FAQ/FAQPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useLocation } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import { Search } from 'lucide-react';
import { faqData } from './FAQContent';
import React, { useMemo } from 'react';
import { faqContent } from './FAQContent';

const HighlightText = ({ text, highlight }: { text: string; highlight: string }) => {
if (!highlight.trim()) {
Expand Down Expand Up @@ -94,117 +96,74 @@ const FAQItem = ({
);
};

const FAQPage = () => {
const [searchQuery, setSearchQuery] = useState('');
const location = useLocation();
const [openItems, setOpenItems] = useState<Record<string, boolean>>(() => {
// Initial state check for hash
const hashId = window.location.hash.replace('#', '');
return hashId ? { [hashId]: true } : {};
});

// Keep open items in sync if hash changes externally
useEffect(() => {
const hashId = location.hash.replace('#', '');
if (hashId && !openItems[hashId]) {
setOpenItems((prev) => ({ ...prev, [hashId]: true }));
}
}, [location.hash]); // eslint-disable-line react-hooks/exhaustive-deps

const filteredFaqs = faqData.filter(
(item) =>
item.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.answer.toLowerCase().includes(searchQuery.toLowerCase())
);
export const FAQPage: React.FC = () => {
// Flatten array and format valid structural JSON-LD matching Schema.org expectations
const jsonLdSchema = useMemo(() => {
const mainEntities = faqContent.flatMap((category) =>
category.items.map((item) => ({
"@type": "Question",
"name": item.question,
"acceptedAnswer": {
"@type": "Answer",
"text": item.answer
}
}))
);

const toggleItem = (id: string) => {
setOpenItems((prev) => {
const isOpening = !prev[id];

// Update hash to reflect state
if (isOpening) {
window.history.pushState(null, '', `#${id}`);
} else if (location.hash === `#${id}`) {
window.history.pushState(null, '', window.location.pathname + window.location.search);
}

return { ...prev, [id]: isOpening };
return JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": mainEntities
});
};

const schemaData = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqData.map((item) => ({
"@type": "Question",
"name": item.question,
"acceptedAnswer": {
"@type": "Answer",
"text": item.answer,
},
})),
};
}, []);

return (
<div className="max-w-3xl mx-auto px-4 py-12">
<Helmet>
<title>FAQ | Tikka</title>
<script type="application/ld+json">
{JSON.stringify(schemaData)}
</script>
</Helmet>
<div className="faq-page-container max-w-4xl mx-auto px-4 py-8">
{/* Dynamic injection of schema structure into the head element */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: jsonLdSchema }}
/>

<header className="mb-10 text-center">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">Help & FAQ</h1>
<p className="text-lg text-gray-600 dark:text-gray-400">
Everything you need to know about Tikka and the Stellar ecosystem.
<h1 className="text-3xl font-bold mb-2">Frequently Asked Questions</h1>
<p className="text-gray-600 dark:text-gray-400">
Everything you need to know about Stellar, Soroban Smart Contracts, and Tikka Raffles.
</p>
</header>

<div className="mb-8 relative max-w-xl mx-auto">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
className="block w-full pl-10 pr-4 py-3 border border-gray-200 dark:border-gray-700 rounded-xl leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm transition-colors shadow-sm"
placeholder="Search for answers..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>

<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6 md:p-8">
{filteredFaqs.length > 0 ? (
filteredFaqs.map((item) => (
<FAQItem
key={item.id}
question={item.question}
answer={item.answer}
id={item.id}
searchQuery={searchQuery}
isOpen={!!openItems[item.id] || (searchQuery.trim().length > 0 && filteredFaqs.length <= 3)}
onToggle={() => toggleItem(item.id)}
/>
))
) : (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400 text-lg">No questions found matching "{searchQuery}"</p>
<button
onClick={() => setSearchQuery('')}
className="mt-4 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium transition-colors"
>
Clear search
</button>
</div>
)}
<div className="faq-categories-wrapper space-y-8">
{faqContent.map((category, catIdx) => (
<section key={catIdx} className="faq-category-block">
<h2 className="text-xl font-semibold border-b pb-2 mb-4 text-primary">
{category.title}
</h2>
<div className="faq-items-list space-y-4">
{category.items.map((item, itemIdx) => (
<details
key={itemIdx}
className="group bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg p-4 transition-all duration-200 cursor-pointer"
>
<summary className="flex justify-between items-center font-medium list-none focus:outline-none select-none">
<span className="text-gray-900 dark:text-gray-100 pr-4">
{item.question}
</span>
{/* Native pure-CSS accordion arrow indicator using group styles */}
<span className="transition-transform duration-200 transform group-open:rotate-180 text-gray-500">
</span>
</summary>
<p className="mt-3 text-sm leading-relaxed text-gray-600 dark:text-gray-400 border-t pt-3 border-gray-100 dark:border-zinc-800 pointer-events-none">
{item.answer}
</p>
</details>
))}
</div>
</section>
))}
</div>

<footer className="mt-12 text-center text-sm text-gray-500">
Still have questions? <a href="mailto:support@tikka.com" className="text-blue-600 hover:underline transition-colors">Contact Support</a>
</footer>
</div>
);
};

export default FAQPage;
export default FAQPage;