mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(home): add disclaimer modal for board navigation
This commit is contained in:
@@ -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 "website"), 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 "I Agree," 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's rules, which you can find on each board'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;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './disclaimer-modal';
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
|||||||
+10
-3
@@ -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,6 +184,8 @@ const Home = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<DisclaimerModal />
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<HomeLogo />
|
<HomeLogo />
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
@@ -189,6 +195,7 @@ const Home = () => {
|
|||||||
<Stats subplebbitAddresses={subplebbitAddresses} />
|
<Stats subplebbitAddresses={subplebbitAddresses} />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user