feat(pass): add pass page and update voting copy

This commit is contained in:
Tommaso Casaburi
2026-04-17 14:21:10 +07:00
parent 4cae9c3caa
commit 1c6cf3fc0f
45 changed files with 501 additions and 97 deletions
+12
View File
@@ -166,6 +166,10 @@ vi.mock('../views/not-found', () => ({
default: makeNamedComponent('not-found-view'),
}));
vi.mock('../views/pass', () => ({
default: makeNamedComponent('pass-view'),
}));
vi.mock('../views/pending-post', () => ({
default: makeNamedComponent('pending-post-view'),
}));
@@ -314,6 +318,14 @@ describe('App', () => {
expect(container.querySelector('[data-testid="not-found-view"]')).toBeTruthy();
});
it('renders the pass route as a global static page', async () => {
await renderApp('/pass');
expect(latestLocation).toBe('/pass');
expect(container.querySelector('[data-testid="pass-view"]')).toBeTruthy();
expect(container.querySelector('[data-testid="boards-bar"]')).toBeNull();
});
it('canonicalizes board address routes to directory codes while preserving query strings', async () => {
await renderApp('/music-posting.eth/thread/comment-1?focus=1');
+2
View File
@@ -40,6 +40,7 @@ import Archive from './views/archive/archive';
import ModQueueView from './views/mod-queue';
import NotAllowed from './views/not-allowed';
import NotFound from './views/not-found';
import Pass from './views/pass';
import PendingPost from './views/pending-post';
import Post from './views/post';
import Rules from './views/rules';
@@ -285,6 +286,7 @@ const App = () => {
<Route element={<GlobalLayout />}>
<Route path='/' element={<Home />} />
<Route path='/faq' element={<FAQ />} />
<Route path='/pass' element={<Pass />} />
<Route path='/rules/:boardIdentifier?' element={<Rules />} />
<Route path='/blotter' element={<Blotter />} />
<Route
@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom';
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
import styles from './create-board-modal.module.css';
@@ -57,14 +58,10 @@ const CreateBoardModal = () => {
</div>
<div className={styles.section}>
<h3>Future: DAO Voting</h3>
<h3>Future: Directory Voting</h3>
<p>
Directory assignments will use gasless pubsub votingcommunities vote, highest-voted board wins the slot. <strong>Voting pages = discovery:</strong> Each
directory&apos;s voting page lists all competing boards (even low-voted ones), giving visibility without winning or dev approval. See{' '}
<a href='https://github.com/pkcprotocol/pkc-js/issues/25' target='_blank' rel='noopener noreferrer'>
design draft
</a>
.
Each directory will have its own voting page listing the boards competing for that slot. Only 5chan users who purchased <Link to='/pass'>5chan Pass</Link>{' '}
will be able to vote.
</p>
</div>
@@ -1,4 +1,4 @@
import { useLocation } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
import styles from './directory-modal.module.css';
@@ -49,7 +49,7 @@ const DirectoryModal = () => {
</a>
. Users can access it anytime via the search bar, direct links, or by subscribing with the &quot;[Subscribe]&quot; button
<strong>no directory assignment or dev approval needed</strong>. Directory boards are simply featured in homepage categories (like &quot;Anime &
Manga&quot;) and are handpicked by devs until DAO curation is implemented.
Manga&quot;) and are handpicked by devs until directory voting is available.
</p>
</div>
@@ -66,14 +66,10 @@ const DirectoryModal = () => {
</div>
<div className={styles.section}>
<h3>Future: DAO Voting</h3>
<h3>Future: Directory Voting</h3>
<p>
Directory assignments will use gasless pubsub votingcommunities vote, highest-voted board wins the slot. <strong>Voting pages = discovery:</strong> Each
directory&apos;s voting page lists all competing boards (even low-voted ones), giving visibility without winning or dev approval. See{' '}
<a href='https://github.com/pkcprotocol/pkc-js/issues/25' target='_blank' rel='noopener noreferrer'>
design draft
</a>
.
Each directory will have its own voting page listing the boards competing for that slot. Only 5chan users who purchased <Link to='/pass'>5chan Pass</Link>{' '}
will be able to vote.
</p>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"title": "/all/ - All 5chan Directories",
"description": "Each 5chan directory is assigned to a board handpicked by the team, until DAO curation is implemented.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories.json",
"description": "Each 5chan directory is assigned to a board handpicked by the team until directory voting is available for 5chan Pass holders.\n\nhttps://github.com/bitsocialnet/lists/blob/master/5chan-directories.json",
"createdAt": 1762179811,
"updatedAt": 1776065598,
"communities": [
+70
View File
@@ -0,0 +1,70 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import Pass from '../pass';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
vi.mock('react-router-hash-link', () => ({
HashLink: ({ children, to }: { children: React.ReactNode; to: string }) => createElement('a', { href: to }, children),
}));
vi.mock('../../home', () => ({
Footer: () => createElement('div', { 'data-testid': 'footer' }, 'footer'),
HomeLogo: () => createElement('div', { 'data-testid': 'home-logo' }, 'home-logo'),
}));
let container: HTMLDivElement;
let root: Root;
const renderPass = async () => {
await act(async () => {
root.render(createElement(MemoryRouter, {}, createElement(Pass)));
});
};
describe('Pass', () => {
beforeEach(() => {
vi.clearAllMocks();
window.scrollTo = vi.fn();
document.title = 'before';
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('renders the placeholder 5chan Pass details and marks the route as coming soon', async () => {
await renderPass();
expect(container.querySelector('[data-testid="home-logo"]')).toBeTruthy();
expect(container.querySelector('[data-testid="footer"]')).toBeTruthy();
expect(container.textContent).toContain('Support 5chan with 5chan Pass');
expect(container.textContent).toContain('not available yet');
expect(container.textContent).toContain('crypto only');
expect(container.textContent).toContain('directory voting');
expect(container.textContent).toContain('/vip/');
expect(container.textContent).toContain('zero-friction');
expect(container.textContent).toContain('CAPTCHA verification');
expect(container.textContent).toContain('$30 for 1 year');
expect(container.textContent).toContain('$60 for 3 years');
expect(container.textContent).toContain('MintPass');
expect(container.textContent).toContain('burn BSO');
expect(container.textContent).toContain('automatically in the smart contract');
expect(container.textContent).toContain('purchase wallet activity can be publicly visible');
expect(container.querySelector('a[href="/rules"]')?.textContent).toBe('rules');
expect(container.querySelector('a[href="/vip"]')?.textContent).toBe('/vip/');
expect(container.querySelector('a[href="https://github.com/bitsocialnet/mintpass"]')?.textContent).toBe('MintPass');
expect(container.querySelector('a[href="https://bitsocial.net"]')?.textContent).toBe('Bitsocial');
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
expect(document.title).toBe('Pass - 5chan');
});
});
+1
View File
@@ -0,0 +1 @@
export { default } from './pass';
+146
View File
@@ -0,0 +1,146 @@
.wrapper {
width: 100%;
}
.content {
width: 100%;
height: 100%;
margin: auto;
text-align: left;
width: 750px;
min-width: 750px;
}
.box {
position: relative;
margin-bottom: 0.5em;
padding-bottom: 0.5em;
border: 1px solid;
}
.boxBar {
padding-left: 0.5em;
line-height: 2em;
text-transform: capitalize;
}
.boxBar h2 {
font-size: 131%;
font-weight: 700;
}
.boxContent {
line-height: 1.5em;
font-size: 93%;
padding: 0.5em;
padding-top: 0.25em;
padding-bottom: 0;
color: #000;
}
.infoBox {
background: var(--homepage-box-background-color);
color: var(--homepage-infobox-text-color);
border: 1px solid var(--homepage-box-border-color);
}
.infoBox .boxBar {
background: var(--homepage-box-bar-background-color);
color: var(--homepage-box-bar-text-color);
}
.infoBox .boxContent a,
.infoBox .boxContent a:visited {
color: var(--homepage-infobox-link-text-color);
text-decoration: underline;
}
.columns::after {
clear: both;
content: '';
display: block;
}
.leftBox {
float: left;
width: 49.1%;
background: #efe;
color: #060;
}
.leftBox .boxBar {
background: #9c6;
color: #060;
}
.rightBox {
float: right;
width: 49.1%;
background: #eff;
color: #006;
}
.rightBox .boxBar {
background: #59a;
color: #fff;
}
.list {
margin: 1em;
margin-left: 4em;
}
.list ul {
margin-left: 1em;
margin-bottom: 0.5em;
}
.box ul li {
list-style: disc outside;
}
.leftBox a:visited {
color: #00e;
text-decoration: underline;
}
.rightBox a,
.rightBox a:visited {
color: #00e;
text-decoration: underline;
}
.rightBox dl {
margin: 1em;
}
.rightBox .first {
padding-top: 0;
}
.rightBox dt {
padding-top: 1em;
margin-bottom: 0.5em;
font-weight: 700;
color: #006;
}
.rightBox dd {
margin-left: 1em;
padding-bottom: 1.5em;
border-bottom: solid #006 1px;
}
@media (max-width: 640px) {
.content {
min-width: 0;
width: auto;
padding: 10px;
}
.rightBox,
.leftBox {
float: none;
width: auto;
}
}
+186
View File
@@ -0,0 +1,186 @@
import { useEffect } from 'react';
import { HashLink } from 'react-router-hash-link';
import { Link } from 'react-router-dom';
import { Footer, HomeLogo } from '../home';
import styles from './pass.module.css';
const Pass = () => {
useEffect(() => {
window.scrollTo(0, 0);
document.title = 'Pass - 5chan';
}, []);
return (
<div className={styles.wrapper}>
<div className={styles.content}>
<HomeLogo />
<div className={`${styles.box} ${styles.infoBox}`}>
<div className={styles.boxBar}>
<h2>Support 5chan with 5chan Pass</h2>
</div>
<div className={styles.boxContent}>
5chan Pass is a planned paid supporter pass for 5chan. It is <strong>not available yet</strong>. When it launches, 5chan Pass holders will be able to vote on
directory pages, post on <Link to='/vip'>/vip/</Link>, and bypass typing a CAPTCHA verification or similar posting challenge on 5chan boards that support the
5chan Pass challenge. Checkout will be <strong>crypto only</strong>. There is no live purchase, activation, or renewal flow today.
</div>
</div>
<div className={styles.columns}>
<div className={`${styles.box} ${styles.leftBox}`}>
<div className={styles.boxBar}>
<h2>Questions</h2>
</div>
<div className={styles.boxContent}>
<ul className={styles.list}>
<li>
<strong>
<HashLink to='#overview'>Overview</HashLink>
</strong>
<ul>
<li>
<HashLink to='#whatpass'>What is 5chan Pass?</HashLink>
</li>
<li>
<HashLink to='#available'>Is it available now?</HashLink>
</li>
<li>
<HashLink to='#whatdoes'>What will it do?</HashLink>
</li>
<li>
<HashLink to='#whatnot'>What will it not do?</HashLink>
</li>
<li>
<HashLink to='#vip'>What is the /vip/ benefit?</HashLink>
</li>
<li>
<HashLink to='#challenge'>How does zero-friction posting work?</HashLink>
</li>
<li>
<HashLink to='#payment'>How will payment work?</HashLink>
</li>
<li>
<HashLink to='#pricing'>Has pricing been announced?</HashLink>
</li>
<li>
<HashLink to='#nft'>How does the NFT work?</HashLink>
</li>
<li>
<HashLink to='#proceeds'>What happens to the proceeds?</HashLink>
</li>
</ul>
</li>
<li>
<strong>
<HashLink to='#faq'>FAQ</HashLink>
</strong>
<ul>
<li>
<HashLink to='#needpass'>Will I need it to browse or post?</HashLink>
</li>
<li>
<HashLink to='#fiat'>Will fiat payments be accepted?</HashLink>
</li>
<li>
<HashLink to='#vpn'>Will VPNs, proxies, or IP changes matter?</HashLink>
</li>
<li>
<HashLink to='#bypass'>Will it bypass bans or board rules?</HashLink>
</li>
<li>
<HashLink to='#privileges'>Will it grant moderation powers or board ownership?</HashLink>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div className={`${styles.box} ${styles.rightBox}`}>
<div className={styles.boxBar}>
<h2 id='overview'>Overview</h2>
</div>
<div className={styles.boxContent}>
<dl>
<dt className={styles.first} id='whatpass'>
What is 5chan Pass?
</dt>
<dd>
5chan Pass is an upcoming supporter purchase for people who want to fund 5chan directly. When it launches, it will be used for directory voting,
zero-friction posting on boards that support the 5chan Pass challenge, and posting on <Link to='/vip'>/vip/</Link>.
</dd>
<dt id='available'>Is it available now?</dt>
<dd>No. This page is a placeholder so the future 5chan Pass route already exists, but there is nothing to buy, activate, or renew yet.</dd>
<dt id='whatdoes'>What will it do?</dt>
<dd>
When it launches, 5chan Pass will let eligible users vote on directory assignments through each directory&apos;s voting page, post on{' '}
<Link to='/vip'>/vip/</Link>, and bypass typing a CAPTCHA verification or similar posting challenge on all 5chan boards that support the pass challenge.
Reporting is not available yet, so the pass does not currently apply to reports.
</dd>
<dt id='whatnot'>What will it not do?</dt>
<dd>
5chan Pass will not give moderation powers, board ownership, special posting privileges, or any way to ignore board-specific{' '}
<Link to='/rules'>rules</Link>. It also will not unlock anything today, because the feature has not launched yet.
</dd>
<dt id='vip'>What is the /vip/ benefit?</dt>
<dd>
Only 5chan Pass users will be able to post on <Link to='/vip'>/vip/</Link>. Browsing rules and long-term direction for that board can still evolve, but
the current plan is that posting there is restricted to pass holders.
</dd>
<dt id='challenge'>How does zero-friction posting work?</dt>
<dd>
On boards that support the 5chan Pass challenge, pass holders will be able to bypass typing a CAPTCHA verification or similar challenge before posting.
This is similar in spirit to how 4chan Pass reduces posting friction, except 5chan reports are not available yet.
</dd>
<dt id='payment'>How will payment work?</dt>
<dd>
5chan Pass will be sold through a crypto-only checkout when it becomes available. No credit cards, PayPal, or other fiat payment methods will be
accepted.
</dd>
<dt id='pricing'>Has pricing been announced?</dt>
<dd>
Yes, the planned pricing model is the same simple structure: Passes cost $30 for 1 year or $60 for 3 years, which is about $1.67 per month or less than
a single 20oz bottle of soda.
</dd>
<dt id='nft'>How does the NFT work?</dt>
<dd>
5chan Pass will be an NFT under the hood, powered by the open-source{' '}
<a href='https://github.com/bitsocialnet/mintpass' target='_blank' rel='noopener noreferrer'>
MintPass
</a>{' '}
service. The 5chan Pass challenge will only accept NFTs minted within the last year, so an NFT older than one year will count as expired until it is
renewed or replaced.
</dd>
<dt id='proceeds'>What happens to the proceeds?</dt>
<dd>
All proceeds from 5chan Pass are intended to burn BSO, the official cryptocurrency token of{' '}
<a href='https://bitsocial.net' target='_blank' rel='noopener noreferrer'>
Bitsocial
</a>
. This is planned to happen automatically in the smart contract. The resulting on-chain transactions will be transparent, so buyers should assume their
purchase wallet activity can be publicly visible.
</dd>
<dt id='faq'>Will I need it to browse or post?</dt>
<dd id='needpass'>
No. 5chan Pass is not required to read boards, subscribe to boards, or post normally. It is for specific benefits like directory voting, posting on{' '}
<Link to='/vip'>/vip/</Link>, and zero-friction posting on boards that support the challenge.
</dd>
<dt id='fiat'>Will fiat payments be accepted?</dt>
<dd>No. When 5chan Pass launches, payment will be crypto only.</dd>
<dt id='vpn'>Will VPNs, proxies, or IP changes matter?</dt>
<dd>
No. Because 5chan Pass is planned as an NFT-based credential, it is not meant to depend on a fixed IP address. There is no intended restriction based on
VPN use, proxies, or changing networks.
</dd>
<dt id='bypass'>Will it bypass bans or board rules?</dt>
<dd>No. A pass is not a way around bans, posting restrictions, or board moderation. Every board still sets and enforces its own rules.</dd>
<dt id='privileges'>Will it grant moderation powers or board ownership?</dt>
<dd>No. Buying a pass will not make someone a moderator, an admin, or the owner of a board. It is a supporter purchase, not a governance override.</dd>
</dl>
</div>
</div>
</div>
<Footer />
</div>
</div>
);
};
export default Pass;