diff --git a/src/app.tsx b/src/app.tsx index c8990f47..6e2f59f3 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -13,6 +13,7 @@ import { getSubplebbitAddress, isPostRoute, isPendingPostRoute } from './lib/uti import styles from './app.module.css'; import FAQ from './views/faq'; import Home from './views/home'; +import Rules from './views/rules'; import NotFound from './views/not-found'; import PendingPost from './views/pending-post'; import Post from './views/post'; @@ -145,6 +146,7 @@ const App = () => ( }> } /> } /> + } /> }> diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index 9f142f20..367f7a73 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -133,6 +133,9 @@ export const Footer = () => {
  • FAQ
  • +
  • + Rules +
  • Telegram diff --git a/src/views/rules/index.ts b/src/views/rules/index.ts new file mode 100644 index 00000000..544e34f2 --- /dev/null +++ b/src/views/rules/index.ts @@ -0,0 +1 @@ +export { default } from './rules'; diff --git a/src/views/rules/rules.module.css b/src/views/rules/rules.module.css new file mode 100644 index 00000000..89455da4 --- /dev/null +++ b/src/views/rules/rules.module.css @@ -0,0 +1,174 @@ +.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; + line-height: 1.5em; + 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); +} + +.selectorBox { + background: #efe; + color: #060; +} + +.selectorBox .boxBar { + background: #9c6; + color: #060; +} + +.selectorRow { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + box-sizing: border-box; +} + +.boardSelect { + padding: 5px 10px; + font-size: 14px; + border: 1px solid #aaa; + border-radius: 0; + background: #fff; + cursor: pointer; + min-width: 200px; + box-sizing: border-box; +} + +.orSeparator { + color: #666; + font-style: italic; +} + +.customAddressForm { + display: flex; + align-items: center; + gap: 5px; + box-sizing: border-box; +} + +.addressInput { + padding: 5px 10px; + font-size: 14px; + border: 1px solid #aaa; + border-radius: 0; + min-width: 200px; + box-sizing: border-box; +} + +.addressInput:focus { + border-color: #ea8; + outline: none; +} + +.goButton { + padding: 5px 10px; + font-size: 14px; + cursor: pointer; + box-sizing: border-box; +} + +.rulesBox { + background: #eff; + color: #006; +} + +.rulesBox .boxBar { + background: #59a; + color: #fff; +} + +.box ol { + margin: 1em; + margin-left: 2em; +} + +.box ol li { + list-style: decimal outside; + margin-bottom: 0.5em; +} + +@media (max-width: 640px) { + .content { + min-width: 0; + width: auto; + padding: 10px; + } + + .selectorRow { + flex-direction: column; + align-items: stretch; + } + + .boardSelect, + .addressInput { + width: 100%; + min-width: 0; + box-sizing: border-box; + } + + .orSeparator { + text-align: center; + } + + .customAddressForm { + flex-direction: column; + width: 100%; + box-sizing: border-box; + } + + .goButton { + box-sizing: border-box; + margin-top: 5px; + padding: 5px 10px; + cursor: pointer; + } +} + +.selectorBoxTitle, .rulesBoxTitle { + text-transform: none; +} \ No newline at end of file diff --git a/src/views/rules/rules.tsx b/src/views/rules/rules.tsx new file mode 100644 index 00000000..61adcffe --- /dev/null +++ b/src/views/rules/rules.tsx @@ -0,0 +1,186 @@ +import { useEffect, useState, FormEvent } from 'react'; +import { useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { Footer, HomeLogo } from '../home'; +import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits'; +import styles from './rules.module.css'; + +const getBoardShortCode = (title?: string): string => { + if (!title) return ''; + const match = title.match(/^\/([^/]+)\//); + return match ? match[1] : ''; +}; + +const getBoardName = (title?: string): string => { + if (!title) return ''; + const match = title.match(/^\/[^/]+\/\s*-\s*(.+)$/); + return match ? match[1] : title; +}; + +const BoardRulesDisplay = ({ subplebbitAddress, defaultSubplebbits }: { subplebbitAddress: string; defaultSubplebbits: MultisubSubplebbit[] }) => { + const subplebbit = useSubplebbit({ subplebbitAddress }); + const { rules, state, title, shortAddress } = subplebbit || {}; + + let loadingText: string | null = null; + if (!subplebbit) { + loadingText = 'connecting...'; + } else { + switch (state) { + case 'fetching-ipns': + case 'fetching-ipfs': + loadingText = 'loading...'; + break; + case 'failed': + loadingText = 'failed to load'; + break; + case 'succeeded': + loadingText = null; + break; + default: + loadingText = state ? `${state}...` : 'loading...'; + } + } + + const isLoaded = state === 'succeeded'; + + const defaultSub = defaultSubplebbits.find((sub) => sub.address === subplebbitAddress); + let displayTitle: string; + if (defaultSub?.title) { + const shortCode = getBoardShortCode(defaultSub.title); + const boardName = getBoardName(defaultSub.title); + displayTitle = `Rules for: /${shortCode}/ - ${boardName}`; + } else if (title) { + const shortCode = getBoardShortCode(title); + const boardName = getBoardName(title); + if (shortCode && boardName && boardName !== title) { + displayTitle = `Rules for: /${shortCode}/ - ${boardName}`; + } else { + displayTitle = `Rules for: ${shortAddress || subplebbitAddress}`; + } + } else { + displayTitle = `Rules for: ${shortAddress || subplebbitAddress}`; + } + + return ( +
    +
    +

    {displayTitle}

    +
    +
    + {!isLoaded ? ( +

    + {loadingText} +

    + ) : rules && rules.length > 0 ? ( +
      + {rules.map((rule, index) => ( +
    1. {rule}
    2. + ))} +
    + ) : ( +

    + This board has no rules set by its owner. +

    + )} +
    +
    + ); +}; + +const BoardSelector = ({ + defaultSubplebbits, + selectedAddress, + onSelect, +}: { + defaultSubplebbits: MultisubSubplebbit[]; + selectedAddress: string; + onSelect: (address: string) => void; +}) => { + const [customAddress, setCustomAddress] = useState(''); + + const handleSelectChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value) { + onSelect(value); + setCustomAddress(''); + } + }; + + const handleCustomSubmit = (e: FormEvent) => { + e.preventDefault(); + if (customAddress.trim()) { + onSelect(customAddress.trim()); + } + }; + + return ( +
    +
    +

    Load rules from a board

    +
    +
    +
    + + or +
    + setCustomAddress(e.target.value)} + className={styles.addressInput} + /> + +
    +
    +
    +
    + ); +}; + +const Rules = () => { + const defaultSubplebbits = useDefaultSubplebbits(); + const [selectedAddress, setSelectedAddress] = useState(''); + + useEffect(() => { + window.scrollTo(0, 0); + document.title = 'Rules - 5chan'; + }, []); + + return ( +
    +
    + +
    +
    +

    Rules

    +
    +
    + 5chan is a serverless and adminless tool to browse and post to decentralized imageboards. 5chan does not have global rules or moderators. Each board is + independently owned and moderated, with its own set of rules determined by the board owner. +
    +
    + Please read and respect the rules of whatever board you decide to post to. +
    +
    + + {selectedAddress && } +
    +
    + ); +}; + +export default Rules;