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 { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Plebbit from '@plebbit/plebbit-js';
|
||||
import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
|
||||
import useIsMobile from '../../../hooks/use-is-mobile';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
|
||||
import styles from './boards-list.module.css';
|
||||
|
||||
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 boardTitle = title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
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 (
|
||||
<tr key={address}>
|
||||
<td>
|
||||
<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>}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
+19
-12
@@ -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 { Trans, useTranslation } from 'react-i18next';
|
||||
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 BoardsList from './boards-list';
|
||||
import Version from '../../components/version';
|
||||
import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store';
|
||||
import DisclaimerModal from '../../components/disclaimer-modal';
|
||||
import _ from 'lodash';
|
||||
|
||||
// https://github.com/plebbit/lists/blob/master/5chan-multisub.json
|
||||
@@ -16,11 +18,13 @@ const SearchBar = () => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { showDisclaimerModal } = useDisclaimerModalStore();
|
||||
|
||||
const handleSearchSubmit = () => {
|
||||
const handleSearchSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const searchInput = searchInputRef.current?.value;
|
||||
if (searchInput) {
|
||||
navigate(`/p/${searchInput}`);
|
||||
showDisclaimerModal(searchInput, navigate);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,15 +184,18 @@ const Home = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<HomeLogo />
|
||||
<SearchBar />
|
||||
<InfoBox />
|
||||
<BoardsList multisub={defaultSubplebbits} />
|
||||
<PopularThreadsBox multisub={defaultSubplebbits} subplebbits={subplebbits} />
|
||||
<Stats subplebbitAddresses={subplebbitAddresses} />
|
||||
<Footer />
|
||||
</div>
|
||||
<>
|
||||
<DisclaimerModal />
|
||||
<div className={styles.content}>
|
||||
<HomeLogo />
|
||||
<SearchBar />
|
||||
<InfoBox />
|
||||
<BoardsList multisub={defaultSubplebbits} />
|
||||
<PopularThreadsBox multisub={defaultSubplebbits} subplebbits={subplebbits} />
|
||||
<Stats subplebbitAddresses={subplebbitAddresses} />
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user