feat(home): add disclaimer modal for board navigation

This commit is contained in:
plebeius
2025-11-04 17:20:01 +01:00
parent e548198d0e
commit 2a25d7353c
6 changed files with 270 additions and 14 deletions
@@ -0,0 +1,102 @@
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(128, 128, 128, 0.5);
z-index: 999;
}
.disclaimerDialog {
position: absolute;
width: 300px;
top: 50px;
left: 50%;
margin-left: -151px;
background-color: rgb(255, 255, 255);
border: 1px solid rgb(136, 0, 0);
color: rgb(136, 0, 0);
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
font-family: arial, helvetica, clean, sans-serif;
font-size: 13px;
line-height: 16.003px;
z-index: 1000;
}
.hd {
background-color: rgb(136, 0, 0);
color: rgb(255, 255, 255);
padding: 4px 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.hd h2 {
margin: 0;
padding: 0;
font-size: 13px;
font-weight: bold;
}
.closeButton {
all: unset;
cursor: pointer;
color: rgb(255, 255, 255);
font-size: 16px;
line-height: 16px;
width: 18px;
height: 18px;
text-align: center;
user-select: none;
}
.closeButton:hover {
opacity: 0.8;
}
.bd {
padding: 10px;
}
.bd p {
margin: 0 0 10px 0;
}
.bd ol {
margin: 0;
padding-left: 20px;
}
.bd li {
margin-bottom: 10px;
}
.bd a {
color: rgb(136, 0, 0);
text-decoration: underline;
}
.disclaimerFooter {
padding: 10px;
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid rgb(136, 0, 0);
}
.disclaimerFooter button {
padding: 4px 12px;
border: 1px solid rgb(136, 0, 0);
background-color: rgb(255, 255, 255);
color: rgb(136, 0, 0);
cursor: pointer;
font-size: 13px;
font-family: arial, helvetica, clean, sans-serif;
}
.disclaimerFooter button:hover {
background-color: rgb(250, 250, 250);
}
@@ -0,0 +1,59 @@
import { useNavigate } from 'react-router-dom';
import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store';
import styles from './disclaimer-modal.module.css';
const DisclaimerModal = () => {
const navigate = useNavigate();
const { showModal, targetAddress, closeDisclaimerModal, acceptDisclaimer } = useDisclaimerModalStore();
if (!showModal) {
return null;
}
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
closeDisclaimerModal();
}
};
const handleAccept = () => {
acceptDisclaimer(navigate);
};
return (
<div className={styles.backdrop} onClick={handleBackdropClick}>
<div className={styles.disclaimerDialog}>
<div className={styles.hd}>
<h2>Disclaimer</h2>
<button className={styles.closeButton} onClick={closeDisclaimerModal} title='Close'>
×
</button>
</div>
<div className={styles.bd}>
<p>To access boards on 5chan (the &quot;website&quot;), you understand and agree to the following:</p>
<ol>
<li>
The content on boards may be for mature audiences only and may not be suitable for minors. Boards are created and moderated by individual owners who run
their own nodes. If you are a minor or it is illegal for you to access mature content, do not proceed.
</li>
<li>
This website is presented to you AS IS, with no warranty, express or implied. By clicking &quot;I Agree,&quot; you agree that 5chan is a decentralized,
serverless, and adminless platform. The website operator does not create, own, moderate, or control any board content, which is stored peer-to-peer and
generated by board owners and users.
</li>
<li>
Each board has its own rules set by its owner. You agree to comply with the individual board&apos;s rules, which you can find on each board&apos;s page.
There are no global rules as 5chan has no central authority.
</li>
</ol>
</div>
<div className={styles.disclaimerFooter}>
<button onClick={handleAccept}>Accept</button>
<button onClick={closeDisclaimerModal}>Cancel</button>
</div>
</div>
</div>
);
};
export default DisclaimerModal;
+1
View File
@@ -0,0 +1 @@
export { default } from './disclaimer-modal';
+75
View File
@@ -0,0 +1,75 @@
import { create } from 'zustand';
import { NavigateFunction } from 'react-router-dom';
const DISCLAIMER_ACCEPTED_KEY = '5chan-disclaimer-accepted';
interface DisclaimerModalState {
showModal: boolean;
targetAddress: string | null;
showDisclaimerModal: (address: string, navigate: NavigateFunction) => void;
closeDisclaimerModal: () => void;
acceptDisclaimer: (navigate: NavigateFunction) => void;
}
const hasAcceptedDisclaimer = (): boolean => {
try {
return localStorage.getItem(DISCLAIMER_ACCEPTED_KEY) === 'true';
} catch (error) {
return false;
}
};
const setDisclaimerAccepted = (): void => {
try {
localStorage.setItem(DISCLAIMER_ACCEPTED_KEY, 'true');
} catch (error) {
console.error('Failed to save disclaimer acceptance to localStorage:', error);
}
};
const useDisclaimerModalStore = create<DisclaimerModalState>((set) => ({
showModal: false,
targetAddress: null,
showDisclaimerModal: (address: string, navigate: NavigateFunction) => {
// Check if user has already accepted the disclaimer
if (hasAcceptedDisclaimer()) {
// Navigate directly without showing modal
navigate(`/p/${address}`);
return;
}
// Show modal if not accepted before
set({
showModal: true,
targetAddress: address,
});
},
closeDisclaimerModal: () => {
set({
showModal: false,
targetAddress: null,
});
},
acceptDisclaimer: (navigate: NavigateFunction) => {
const state = useDisclaimerModalStore.getState();
// Save acceptance to localStorage
setDisclaimerAccepted();
// Navigate to the target address
if (state.targetAddress) {
navigate(`/p/${state.targetAddress}`);
}
// Close the modal
set({
showModal: false,
targetAddress: null,
});
},
}));
export default useDisclaimerModalStore;
+14 -2
View File
@@ -1,10 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Plebbit from '@plebbit/plebbit-js'; import Plebbit from '@plebbit/plebbit-js';
import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits'; import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
import useIsMobile from '../../../hooks/use-is-mobile'; import useIsMobile from '../../../hooks/use-is-mobile';
import LoadingEllipsis from '../../../components/loading-ellipsis'; import LoadingEllipsis from '../../../components/loading-ellipsis';
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
import styles from './boards-list.module.css'; import styles from './boards-list.module.css';
const Board = ({ subplebbit, isMobile }: { subplebbit: MultisubSubplebbit; isMobile: boolean }) => { const Board = ({ subplebbit, isMobile }: { subplebbit: MultisubSubplebbit; isMobile: boolean }) => {
@@ -12,12 +13,23 @@ const Board = ({ subplebbit, isMobile }: { subplebbit: MultisubSubplebbit; isMob
const { address, title, nsfw } = subplebbit || {}; const { address, title, nsfw } = subplebbit || {};
const boardTitle = title?.replace(/^\/[^/]+\/\s*-\s*/, '') || ''; const boardTitle = title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
const displayAddress = address && Plebbit.getShortAddress(address); const displayAddress = address && Plebbit.getShortAddress(address);
const navigate = useNavigate();
const { showDisclaimerModal } = useDisclaimerModalStore();
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (address) {
showDisclaimerModal(address, navigate);
}
};
return ( return (
<tr key={address}> <tr key={address}>
<td> <td>
<p className={styles.boardCell}> <p className={styles.boardCell}>
<Link to={`/p/${address}`}>{displayAddress}</Link> <Link to={`/p/${address}`} onClick={handleLinkClick}>
{displayAddress}
</Link>
{nsfw && <span className={styles.nsfw}> ({t('nsfw')})</span>} {nsfw && <span className={styles.nsfw}> ({t('nsfw')})</span>}
</p> </p>
</td> </td>
+19 -12
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef, FormEvent } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useSubplebbits } from '@plebbit/plebbit-react-hooks'; import { useSubplebbits } from '@plebbit/plebbit-react-hooks';
@@ -8,6 +8,8 @@ import useSubplebbitsStats from '../../hooks/use-subplebbits-stats';
import PopularThreadsBox from './popular-threads-box'; import PopularThreadsBox from './popular-threads-box';
import BoardsList from './boards-list'; import BoardsList from './boards-list';
import Version from '../../components/version'; import Version from '../../components/version';
import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store';
import DisclaimerModal from '../../components/disclaimer-modal';
import _ from 'lodash'; import _ from 'lodash';
// https://github.com/plebbit/lists/blob/master/5chan-multisub.json // https://github.com/plebbit/lists/blob/master/5chan-multisub.json
@@ -16,11 +18,13 @@ const SearchBar = () => {
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { showDisclaimerModal } = useDisclaimerModalStore();
const handleSearchSubmit = () => { const handleSearchSubmit = (e: FormEvent) => {
e.preventDefault();
const searchInput = searchInputRef.current?.value; const searchInput = searchInputRef.current?.value;
if (searchInput) { if (searchInput) {
navigate(`/p/${searchInput}`); showDisclaimerModal(searchInput, navigate);
} }
}; };
@@ -180,15 +184,18 @@ const Home = () => {
}, []); }, []);
return ( return (
<div className={styles.content}> <>
<HomeLogo /> <DisclaimerModal />
<SearchBar /> <div className={styles.content}>
<InfoBox /> <HomeLogo />
<BoardsList multisub={defaultSubplebbits} /> <SearchBar />
<PopularThreadsBox multisub={defaultSubplebbits} subplebbits={subplebbits} /> <InfoBox />
<Stats subplebbitAddresses={subplebbitAddresses} /> <BoardsList multisub={defaultSubplebbits} />
<Footer /> <PopularThreadsBox multisub={defaultSubplebbits} subplebbits={subplebbits} />
</div> <Stats subplebbitAddresses={subplebbitAddresses} />
<Footer />
</div>
</>
); );
}; };