diff --git a/src/components/disclaimer-modal/disclaimer-modal.module.css b/src/components/disclaimer-modal/disclaimer-modal.module.css new file mode 100644 index 00000000..1bf00443 --- /dev/null +++ b/src/components/disclaimer-modal/disclaimer-modal.module.css @@ -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); +} + diff --git a/src/components/disclaimer-modal/disclaimer-modal.tsx b/src/components/disclaimer-modal/disclaimer-modal.tsx new file mode 100644 index 00000000..a2ca3b24 --- /dev/null +++ b/src/components/disclaimer-modal/disclaimer-modal.tsx @@ -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) => { + if (e.target === e.currentTarget) { + closeDisclaimerModal(); + } + }; + + const handleAccept = () => { + acceptDisclaimer(navigate); + }; + + return ( +
+
+
+

Disclaimer

+ +
+
+

To access boards on 5chan (the "website"), you understand and agree to the following:

+
    +
  1. + 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. +
  2. +
  3. + 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. +
  4. +
  5. + 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. +
  6. +
+
+
+ + +
+
+
+ ); +}; + +export default DisclaimerModal; diff --git a/src/components/disclaimer-modal/index.ts b/src/components/disclaimer-modal/index.ts new file mode 100644 index 00000000..419f494c --- /dev/null +++ b/src/components/disclaimer-modal/index.ts @@ -0,0 +1 @@ +export { default } from './disclaimer-modal'; diff --git a/src/stores/use-disclaimer-modal-store.ts b/src/stores/use-disclaimer-modal-store.ts new file mode 100644 index 00000000..89bc3240 --- /dev/null +++ b/src/stores/use-disclaimer-modal-store.ts @@ -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((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; diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index 68121c2f..7ea7b2c2 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -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) => { + e.preventDefault(); + if (address) { + showDisclaimerModal(address, navigate); + } + }; return (

- {displayAddress} + + {displayAddress} + {nsfw && ({t('nsfw')})}

diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index e4e5b0e7..093233f3 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -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(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 ( -
- - - - - - -
-
+ <> + +
+ + + + + + +
+ ); };