mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(pass): add pass page and update voting copy
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
@@ -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 voting—communities vote, highest-voted board wins the slot. <strong>Voting pages = discovery:</strong> Each
|
||||
directory'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 "[Subscribe]" button—
|
||||
<strong>no directory assignment or dev approval needed</strong>. Directory boards are simply featured in homepage categories (like "Anime &
|
||||
Manga") and are handpicked by devs until DAO curation is implemented.
|
||||
Manga") 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 voting—communities vote, highest-voted board wins the slot. <strong>Voting pages = discovery:</strong> Each
|
||||
directory'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,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": [
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './pass';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'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;
|
||||
Reference in New Issue
Block a user