mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(topbar): add customizable visibility controls for directories and subscriptions
This commit is contained in:
@@ -26,6 +26,9 @@ import ReplyModal from './components/reply-modal';
|
|||||||
import PostForm from './components/post-form';
|
import PostForm from './components/post-form';
|
||||||
import SubplebbitStats from './components/subplebbit-stats';
|
import SubplebbitStats from './components/subplebbit-stats';
|
||||||
import TopBar from './components/topbar';
|
import TopBar from './components/topbar';
|
||||||
|
import TopbarEditModal from './components/topbar-edit-modal';
|
||||||
|
import DirectoryModal from './components/directory-modal';
|
||||||
|
import DisclaimerModal from './components/disclaimer-modal';
|
||||||
import SettingsModal from './components/settings-modal';
|
import SettingsModal from './components/settings-modal';
|
||||||
|
|
||||||
const BoardLayout = () => {
|
const BoardLayout = () => {
|
||||||
@@ -65,6 +68,9 @@ const BoardLayout = () => {
|
|||||||
<div className={styles.boardLayout}>
|
<div className={styles.boardLayout}>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<CreateBoardModal />
|
<CreateBoardModal />
|
||||||
|
<TopbarEditModal />
|
||||||
|
<DirectoryModal />
|
||||||
|
<DisclaimerModal />
|
||||||
<BoardHeader />
|
<BoardHeader />
|
||||||
{isMobile
|
{isMobile
|
||||||
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
|
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
|
||||||
|
|||||||
@@ -4,19 +4,23 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(128, 128, 128, 0.5);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backdropHome {
|
||||||
|
background-color: rgba(128, 128, 128, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.directoryDialog {
|
.directoryDialog {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -251px;
|
margin-left: -251px;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: var(--directory-modal-background-color, #f0e0d6);
|
||||||
border: 1px solid rgb(136, 0, 0);
|
border: var(--directory-modal-border, 1px solid #d9bfb7);
|
||||||
color: rgb(136, 0, 0);
|
color: var(--directory-modal-text-color, #800);
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
|
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
|
||||||
font-family: arial, helvetica, clean, sans-serif;
|
font-family: arial, helvetica, clean, sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -35,8 +39,8 @@
|
|||||||
|
|
||||||
.hd {
|
.hd {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: rgb(136, 0, 0);
|
background-color: var(--directory-modal-header-background-color, #ea8);
|
||||||
color: rgb(255, 255, 255);
|
color: var(--directory-modal-header-text-color, #800);
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -58,7 +62,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
background-image: var(--disclaimer-modal-close-button-background-image);
|
background-image: var(--directory-modal-close-button-background-image, url("/assets/buttons/cross-red.png"));
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@@ -91,7 +95,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: rgb(136, 0, 0);
|
color: var(--directory-modal-text-color, #800);
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +110,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bd a {
|
.bd a {
|
||||||
color: rgb(136, 0, 0);
|
color: var(--directory-modal-text-color, #800);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
||||||
import styles from './directory-modal.module.css';
|
import styles from './directory-modal.module.css';
|
||||||
|
|
||||||
const DirectoryModal = () => {
|
const DirectoryModal = () => {
|
||||||
const { showModal, closeDirectoryModal } = useDirectoryModalStore();
|
const { showModal, closeDirectoryModal } = useDirectoryModalStore();
|
||||||
|
const location = useLocation();
|
||||||
|
const isHomeView = location.pathname === '/';
|
||||||
|
|
||||||
if (!showModal) {
|
if (!showModal) {
|
||||||
return null;
|
return null;
|
||||||
@@ -15,7 +18,7 @@ const DirectoryModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.backdrop} onClick={handleBackdropClick}>
|
<div className={`${styles.backdrop} ${isHomeView ? styles.backdropHome : ''}`} onClick={handleBackdropClick}>
|
||||||
<div className={styles.directoryDialog}>
|
<div className={styles.directoryDialog}>
|
||||||
<div className={styles.hd}>
|
<div className={styles.hd}>
|
||||||
<h2>Submit a Board to a Directory</h2>
|
<h2>Submit a Board to a Directory</h2>
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
top: 50px;
|
top: 50px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -151px;
|
margin-left: -151px;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: var(--disclaimer-modal-background-color, rgb(255, 255, 255));
|
||||||
border: 1px solid rgb(136, 0, 0);
|
border: var(--disclaimer-modal-border, 1px solid rgb(136, 0, 0));
|
||||||
color: rgb(136, 0, 0);
|
color: var(--disclaimer-modal-text-color, rgb(136, 0, 0));
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
|
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
|
||||||
font-family: arial, helvetica, clean, sans-serif;
|
font-family: arial, helvetica, clean, sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
.hd {
|
.hd {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: rgb(136, 0, 0);
|
background-color: var(--disclaimer-modal-header-background-color, rgb(136, 0, 0));
|
||||||
color: rgb(255, 255, 255);
|
color: var(--disclaimer-modal-header-text-color, rgb(255, 255, 255));
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bd a {
|
.bd a {
|
||||||
color: rgb(136, 0, 0);
|
color: var(--disclaimer-modal-text-color, rgb(136, 0, 0));
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store';
|
import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store';
|
||||||
|
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
||||||
import styles from './disclaimer-modal.module.css';
|
import styles from './disclaimer-modal.module.css';
|
||||||
|
|
||||||
const DisclaimerModal = () => {
|
const DisclaimerModal = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { showModal, closeDisclaimerModal, acceptDisclaimer } = useDisclaimerModalStore();
|
const { showModal, closeDisclaimerModal, acceptDisclaimer, targetBoardPath } = useDisclaimerModalStore();
|
||||||
|
const { openDirectoryModal } = useDirectoryModalStore();
|
||||||
|
|
||||||
if (!showModal) {
|
if (!showModal) {
|
||||||
return null;
|
return null;
|
||||||
@@ -17,7 +19,13 @@ const DisclaimerModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
acceptDisclaimer(navigate);
|
// If there's no board path (placeholder), show directory modal instead
|
||||||
|
if (!targetBoardPath) {
|
||||||
|
closeDisclaimerModal();
|
||||||
|
openDirectoryModal();
|
||||||
|
} else {
|
||||||
|
acceptDisclaimer(navigate);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './topbar-edit-modal';
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbarEditDialog {
|
||||||
|
position: absolute;
|
||||||
|
width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
top: 50px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -300px;
|
||||||
|
background-color: var(--settings-modal-background-color);
|
||||||
|
border-top: var(--settings-modal-border-top);
|
||||||
|
border-right: var(--settings-modal-border-right);
|
||||||
|
border-bottom: var(--settings-modal-border-bottom);
|
||||||
|
border-left: var(--settings-modal-border-left);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px;
|
||||||
|
font-size: var(--body-font-size);
|
||||||
|
line-height: 16.003px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.topbarEditDialog {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
margin-left: -50%;
|
||||||
|
left: 50%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd {
|
||||||
|
position: relative;
|
||||||
|
padding: 4px 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--settings-modal-header-border-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hd h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 131%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background-image: var(--settings-modal-close-button-background-image);
|
||||||
|
background-size: 100%;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: calc(var(--body-font-size) * 1.077);
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directoryInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: var(--body-font-size);
|
||||||
|
border: var(--reply-modal-field-input-border, 1px solid #aaa);
|
||||||
|
outline: none;
|
||||||
|
font-family: var(--body-font-family, arial, helvetica, sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.directoryInput:focus {
|
||||||
|
border: var(--reply-modal-field-input-border-focus, 1px solid #ea8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxItem input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxItem label {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--body-font-size);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbarEditFooter {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
border-top: var(--settings-modal-header-border-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbarEditFooter button {
|
||||||
|
padding: 4px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--body-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import Plebbit from '@plebbit/plebbit-js';
|
||||||
|
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
|
||||||
|
import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
|
||||||
|
import { getAllBoardCodes } from '../../constants/board-codes';
|
||||||
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||||
|
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
||||||
|
import styles from './topbar-edit-modal.module.css';
|
||||||
|
|
||||||
|
const TopbarEditModal = () => {
|
||||||
|
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
|
||||||
|
const { visibleDirectories, visibleSubscriptions, setDirectoryVisibility, setSubscriptionVisibility } = useTopbarVisibilityStore();
|
||||||
|
const account = useAccount();
|
||||||
|
const subscriptions = account?.subscriptions || [];
|
||||||
|
const defaultSubplebbits = useDefaultSubplebbits();
|
||||||
|
|
||||||
|
// Convert visible directories set to space-separated string for input
|
||||||
|
const directoriesToString = (dirs: Set<string>): string => {
|
||||||
|
return Array.from(dirs).sort().join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert space-separated string to set of directory codes
|
||||||
|
const stringToDirectories = (str: string): Set<string> => {
|
||||||
|
const codes = str
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((code) => code.length > 0)
|
||||||
|
.map((code) => code.toLowerCase());
|
||||||
|
return new Set(codes);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Local state for text input (will be saved on Save click)
|
||||||
|
const [localDirectoryInput, setLocalDirectoryInput] = useState<string>(directoriesToString(visibleDirectories));
|
||||||
|
const [localSubscriptionVisibility, setLocalSubscriptionVisibility] = useState<Set<string>>(visibleSubscriptions);
|
||||||
|
|
||||||
|
// Update local state when modal opens or store changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (showModal) {
|
||||||
|
setLocalDirectoryInput(directoriesToString(visibleDirectories));
|
||||||
|
setLocalSubscriptionVisibility(new Set(visibleSubscriptions));
|
||||||
|
}
|
||||||
|
}, [showModal, visibleDirectories, visibleSubscriptions]);
|
||||||
|
|
||||||
|
if (!showModal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
closeTopbarEditModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscriptionToggle = (address: string) => {
|
||||||
|
const newSet = new Set(localSubscriptionVisibility);
|
||||||
|
if (newSet.has(address)) {
|
||||||
|
newSet.delete(address);
|
||||||
|
} else {
|
||||||
|
newSet.add(address);
|
||||||
|
}
|
||||||
|
setLocalSubscriptionVisibility(newSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// Parse directory input and apply changes
|
||||||
|
const inputDirectories = stringToDirectories(localDirectoryInput);
|
||||||
|
const allCodes = getAllBoardCodes();
|
||||||
|
|
||||||
|
// Hide all directories first, then show only the ones in the input
|
||||||
|
allCodes.forEach((code) => {
|
||||||
|
setDirectoryVisibility(code, inputDirectories.has(code));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply subscription visibility changes
|
||||||
|
subscriptions.forEach((address: string) => {
|
||||||
|
setSubscriptionVisibility(address, localSubscriptionVisibility.has(address));
|
||||||
|
});
|
||||||
|
|
||||||
|
closeTopbarEditModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
// Reset local state to match store
|
||||||
|
setLocalDirectoryInput(directoriesToString(visibleDirectories));
|
||||||
|
setLocalSubscriptionVisibility(new Set(visibleSubscriptions));
|
||||||
|
closeTopbarEditModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.backdrop} onClick={handleBackdropClick}>
|
||||||
|
<div className={styles.topbarEditDialog}>
|
||||||
|
<div className={styles.hd}>
|
||||||
|
<h2>Custom Board List</h2>
|
||||||
|
<button className={styles.closeButton} onClick={handleCancel} title='Close' />
|
||||||
|
</div>
|
||||||
|
<div className={styles.bd}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3>Directory Boards</h3>
|
||||||
|
<p>Enter directory codes separated by spaces (e.g., "jp tg mu"):</p>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className={styles.directoryInput}
|
||||||
|
placeholder='Example: jp tg mu'
|
||||||
|
value={localDirectoryInput}
|
||||||
|
onChange={(e) => setLocalDirectoryInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subscriptions.length > 0 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3>Subscriptions</h3>
|
||||||
|
<p>Select which subscriptions to show in the topbar:</p>
|
||||||
|
<div className={styles.checkboxGroup}>
|
||||||
|
{subscriptions.map((address: string) => {
|
||||||
|
const boardPath = getBoardPath(address, defaultSubplebbits);
|
||||||
|
const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : Plebbit.getShortAddress(address);
|
||||||
|
const isChecked = localSubscriptionVisibility.has(address);
|
||||||
|
return (
|
||||||
|
<div key={address} className={styles.checkboxItem}>
|
||||||
|
<input type='checkbox' id={`subscription-${address}`} checked={isChecked} onChange={() => handleSubscriptionToggle(address)} />
|
||||||
|
<label htmlFor={`subscription-${address}`}>{boardPath || displayText}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.topbarEditFooter}>
|
||||||
|
<button onClick={handleSave}>Save</button>
|
||||||
|
<button onClick={handleCancel}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopbarEditModal;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Types for topbar edit modal component
|
||||||
|
|
||||||
|
export interface BoardCodeCheckbox {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionCheckbox {
|
||||||
|
address: string;
|
||||||
|
displayName: string;
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--topbar-desktop-link-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
.navTopRight {
|
.navTopRight {
|
||||||
float: right;
|
float: right;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import Plebbit from '@plebbit/plebbit-js';
|
import Plebbit from '@plebbit/plebbit-js';
|
||||||
import { useAccount, useAccountComment, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
import { useAccount, useAccountComment, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||||
import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import { useDefaultSubplebbitAddresses, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
|
import { useDefaultSubplebbitAddresses, useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
|
||||||
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
|
||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
|
||||||
import { TimeFilter } from '../board-buttons';
|
import { TimeFilter } from '../board-buttons';
|
||||||
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
|
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
|
||||||
|
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
|
||||||
|
import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
|
||||||
|
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
||||||
|
import { BOARD_CODE_GROUPS } from '../../constants/board-codes';
|
||||||
import styles from './topbar.module.css';
|
import styles from './topbar.module.css';
|
||||||
import _, { debounce } from 'lodash';
|
import _, { debounce } from 'lodash';
|
||||||
|
|
||||||
@@ -71,6 +75,16 @@ const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to find board address by directory code
|
||||||
|
const findBoardAddressByCode = (code: string, defaultSubplebbits: MultisubSubplebbit[]): string | null => {
|
||||||
|
const entry = defaultSubplebbits.find((subplebbit) => {
|
||||||
|
if (!subplebbit.title) return false;
|
||||||
|
const directory = extractDirectoryFromTitle(subplebbit.title);
|
||||||
|
return directory === code;
|
||||||
|
});
|
||||||
|
return entry?.address || null;
|
||||||
|
};
|
||||||
|
|
||||||
const TopBarDesktop = () => {
|
const TopBarDesktop = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
@@ -79,13 +93,72 @@ const TopBarDesktop = () => {
|
|||||||
const isInCatalogView = isCatalogView(location.pathname, params);
|
const isInCatalogView = isCatalogView(location.pathname, params);
|
||||||
const [showSearchBar, setShowSearchBar] = useState(false);
|
const [showSearchBar, setShowSearchBar] = useState(false);
|
||||||
const { openCreateBoardModal } = useCreateBoardModalStore();
|
const { openCreateBoardModal } = useCreateBoardModalStore();
|
||||||
|
const { openTopbarEditModal } = useTopbarEditModalStore();
|
||||||
|
const { openDirectoryModal } = useDirectoryModalStore();
|
||||||
|
const { visibleDirectories, visibleSubscriptions } = useTopbarVisibilityStore();
|
||||||
const defaultSubplebbits = useDefaultSubplebbits();
|
const defaultSubplebbits = useDefaultSubplebbits();
|
||||||
|
|
||||||
const subscriptions = account?.subscriptions;
|
const subscriptions = account?.subscriptions || [];
|
||||||
|
|
||||||
const { accountSubplebbits } = useAccountSubplebbits();
|
const { accountSubplebbits } = useAccountSubplebbits();
|
||||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||||
|
|
||||||
|
// Filter subscriptions to only show visible ones
|
||||||
|
const visibleSubscriptionAddresses = subscriptions.filter((address: string) => visibleSubscriptions.has(address));
|
||||||
|
|
||||||
|
// Initialize visibility store on mount
|
||||||
|
useEffect(() => {
|
||||||
|
useTopbarVisibilityStore.getState().initialize();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Render a board code link or placeholder
|
||||||
|
const renderBoardCode = (code: string, isLastInGroup: boolean) => {
|
||||||
|
const address = findBoardAddressByCode(code, defaultSubplebbits);
|
||||||
|
const isPlaceholder = !address;
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
// If no address exists, prevent navigation and open directory modal
|
||||||
|
if (!address) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openDirectoryModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkContent = (
|
||||||
|
<>
|
||||||
|
{isPlaceholder ? (
|
||||||
|
<span className={styles.placeholder} onClick={handleClick} style={{ cursor: 'pointer' }}>
|
||||||
|
{code}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Link to={`/${code}${isInCatalogView ? '/catalog' : ''}`} onClick={handleClick}>
|
||||||
|
{code}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={code}>
|
||||||
|
{linkContent}
|
||||||
|
{!isLastInGroup && ' / '}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a subscription link
|
||||||
|
const renderSubscription = (address: string, index: number, total: number) => {
|
||||||
|
const boardPath = getBoardPath(address, defaultSubplebbits);
|
||||||
|
const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : Plebbit.getShortAddress(address);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={address}>
|
||||||
|
{boardPath && boardPath.trim() ? <Link to={`/${boardPath}${isInCatalogView ? '/catalog' : ''}`}>{displayText}</Link> : <span>{displayText}</span>}
|
||||||
|
{index !== total - 1 && ' / '}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.boardNavDesktop}>
|
<div className={styles.boardNavDesktop}>
|
||||||
<span className={styles.boardList}>
|
<span className={styles.boardList}>
|
||||||
@@ -97,24 +170,20 @@ const TopBarDesktop = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
]{' '}
|
]{' '}
|
||||||
{subscriptions?.length > 0 && (
|
{BOARD_CODE_GROUPS.map((group, groupIndex) => {
|
||||||
<>
|
const visibleCodes = group.filter((code) => visibleDirectories.has(code));
|
||||||
[
|
if (visibleCodes.length === 0) return null;
|
||||||
{subscriptions.map((address: any, index: any) => {
|
|
||||||
const boardPath = getBoardPath(address, defaultSubplebbits);
|
return <span key={groupIndex}>[{visibleCodes.map((code, codeIndex) => renderBoardCode(code, codeIndex === visibleCodes.length - 1))}] </span>;
|
||||||
const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : address.slice(0, 10).concat('...');
|
})}
|
||||||
return (
|
{visibleSubscriptionAddresses.length > 0 && (
|
||||||
<span key={index}>
|
<>[{visibleSubscriptionAddresses.map((address: string, index: number) => renderSubscription(address, index, visibleSubscriptionAddresses.length))}] </>
|
||||||
{index === 0 ? null : ' '}
|
|
||||||
{boardPath && boardPath.trim() ? <Link to={`/${boardPath}${isInCatalogView ? '/catalog' : ''}`}>{displayText}</Link> : <span>{displayText}</span>}
|
|
||||||
{index !== subscriptions?.length - 1 ? ' /' : null}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
]{' '}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
[
|
[
|
||||||
|
<span className={styles.temporaryButton} onClick={() => openTopbarEditModal()} style={{ cursor: 'pointer' }}>
|
||||||
|
{_.capitalize(t('edit'))}
|
||||||
|
</span>
|
||||||
|
] [
|
||||||
<span className={styles.temporaryButton} onClick={() => openCreateBoardModal()} style={{ cursor: 'pointer' }}>
|
<span className={styles.temporaryButton} onClick={() => openCreateBoardModal()} style={{ cursor: 'pointer' }}>
|
||||||
{t('create_board')}
|
{t('create_board')}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Mapping of 4chan board codes to 5chan directory names
|
||||||
|
* Used for displaying board links in the topbar
|
||||||
|
*/
|
||||||
|
export const BOARD_CODE_TO_DIRECTORY: Record<string, string> = {
|
||||||
|
a: 'Anime & Manga',
|
||||||
|
b: 'Random',
|
||||||
|
c: 'Anime/Cute',
|
||||||
|
d: 'Mecha',
|
||||||
|
e: 'Ecchi',
|
||||||
|
f: 'Flash',
|
||||||
|
g: 'Technology',
|
||||||
|
gif: 'Adult GIF',
|
||||||
|
h: 'Hentai',
|
||||||
|
hr: 'High Resolution',
|
||||||
|
k: 'Traditional Games',
|
||||||
|
m: 'Mecha',
|
||||||
|
o: 'Auto',
|
||||||
|
p: 'Photography',
|
||||||
|
r: 'Request',
|
||||||
|
s: 'Sexy Beautiful Women',
|
||||||
|
t: 'Torrents',
|
||||||
|
u: 'Yuri',
|
||||||
|
v: 'Video Games',
|
||||||
|
vg: 'Video Game Generals',
|
||||||
|
vm: 'Video Games/Multiplayer',
|
||||||
|
vmg: 'Video Games/Mobile',
|
||||||
|
vr: 'Retro Games',
|
||||||
|
vrpg: 'Video Games/RPG',
|
||||||
|
vst: 'Video Games/Strategy',
|
||||||
|
w: 'Anime/Wallpapers',
|
||||||
|
wg: 'Worksafe GIF',
|
||||||
|
i: 'Oekaki',
|
||||||
|
ic: 'Artwork/Critique',
|
||||||
|
r9k: 'ROBOT9001',
|
||||||
|
s4s: 'Shit 4chan Says',
|
||||||
|
vip: 'Very Important Posts',
|
||||||
|
cm: 'Cute/Male',
|
||||||
|
hm: 'Handsome Men',
|
||||||
|
lgbt: 'LGBT',
|
||||||
|
y: 'Yaoi',
|
||||||
|
'3': '3DCG',
|
||||||
|
aco: 'Anime/Cute',
|
||||||
|
adv: 'Advertisement',
|
||||||
|
an: 'Animals & Nature',
|
||||||
|
bant: 'International/Random',
|
||||||
|
biz: 'Business & Finance',
|
||||||
|
cgl: 'Cosplay & EGL',
|
||||||
|
ck: 'Food & Cooking',
|
||||||
|
co: 'Comics & Cartoons',
|
||||||
|
diy: 'Do-It-Yourself',
|
||||||
|
fa: 'Fashion',
|
||||||
|
fit: 'Fitness',
|
||||||
|
gd: 'Graphic Design',
|
||||||
|
hc: 'Hardcore',
|
||||||
|
his: 'History & Humanities',
|
||||||
|
int: 'International',
|
||||||
|
jp: 'Otaku Culture',
|
||||||
|
lit: 'Literature',
|
||||||
|
mlp: 'Pony',
|
||||||
|
mu: 'Music',
|
||||||
|
n: 'Transportation',
|
||||||
|
news: 'Current News',
|
||||||
|
out: 'Outdoors',
|
||||||
|
po: 'Papercraft & Origami',
|
||||||
|
pol: 'Politically Incorrect',
|
||||||
|
pw: 'Professional Wrestling',
|
||||||
|
qst: 'Quests',
|
||||||
|
sci: 'Science & Math',
|
||||||
|
soc: 'Cams & Meetups',
|
||||||
|
sp: 'Sports',
|
||||||
|
tg: 'Traditional Games',
|
||||||
|
toy: 'Toys',
|
||||||
|
trv: 'Travel',
|
||||||
|
tv: 'Television & Film',
|
||||||
|
vp: 'Pokémon',
|
||||||
|
vt: 'Virtual YouTubers',
|
||||||
|
wsg: 'Worksafe Requests',
|
||||||
|
wsr: 'Worksafe Requests',
|
||||||
|
x: 'Adult Cartoons',
|
||||||
|
xs: 'Extreme Sports',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Board code groups matching 4chan's bracket structure
|
||||||
|
* Each group represents boards displayed together in brackets
|
||||||
|
*/
|
||||||
|
export const BOARD_CODE_GROUPS: string[][] = [
|
||||||
|
// Group 1: [a / b / c / d / e / f / g / gif / h / hr / k / m / o / p / r / s / t / u / v / vg / vm / vmg / vr / vrpg / vst / w / wg]
|
||||||
|
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'gif', 'h', 'hr', 'k', 'm', 'o', 'p', 'r', 's', 't', 'u', 'v', 'vg', 'vm', 'vmg', 'vr', 'vrpg', 'vst', 'w', 'wg'],
|
||||||
|
// Group 2: [i / ic]
|
||||||
|
['i', 'ic'],
|
||||||
|
// Group 3: [r9k / s4s / vip]
|
||||||
|
['r9k', 's4s', 'vip'],
|
||||||
|
// Group 4: [cm / hm / lgbt / y]
|
||||||
|
['cm', 'hm', 'lgbt', 'y'],
|
||||||
|
// Group 5: [3 / aco / adv / an / bant / biz / cgl / ck / co / diy / fa / fit / gd / hc / his / int / jp / lit / mlp / mu / n / news / out / po / pol / pw / qst / sci / soc / sp / tg / toy / trv / tv / vp / vt / wsg / wsr / x / xs]
|
||||||
|
[
|
||||||
|
'3',
|
||||||
|
'aco',
|
||||||
|
'adv',
|
||||||
|
'an',
|
||||||
|
'bant',
|
||||||
|
'biz',
|
||||||
|
'cgl',
|
||||||
|
'ck',
|
||||||
|
'co',
|
||||||
|
'diy',
|
||||||
|
'fa',
|
||||||
|
'fit',
|
||||||
|
'gd',
|
||||||
|
'hc',
|
||||||
|
'his',
|
||||||
|
'int',
|
||||||
|
'jp',
|
||||||
|
'lit',
|
||||||
|
'mlp',
|
||||||
|
'mu',
|
||||||
|
'n',
|
||||||
|
'news',
|
||||||
|
'out',
|
||||||
|
'po',
|
||||||
|
'pol',
|
||||||
|
'pw',
|
||||||
|
'qst',
|
||||||
|
'sci',
|
||||||
|
'soc',
|
||||||
|
'sp',
|
||||||
|
'tg',
|
||||||
|
'toy',
|
||||||
|
'trv',
|
||||||
|
'tv',
|
||||||
|
'vp',
|
||||||
|
'vt',
|
||||||
|
'wsg',
|
||||||
|
'wsr',
|
||||||
|
'x',
|
||||||
|
'xs',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all board codes as a flat array
|
||||||
|
*/
|
||||||
|
export const getAllBoardCodes = (): string[] => {
|
||||||
|
return BOARD_CODE_GROUPS.flat();
|
||||||
|
};
|
||||||
@@ -33,12 +33,17 @@ const useDisclaimerModalStore = create<DisclaimerModalState>((set) => ({
|
|||||||
targetAddress: null,
|
targetAddress: null,
|
||||||
targetBoardPath: null,
|
targetBoardPath: null,
|
||||||
|
|
||||||
showDisclaimerModal: (address: string, navigate: NavigateFunction, boardPath?: string) => {
|
showDisclaimerModal: (address: string, navigate: NavigateFunction, boardPath?: string | null) => {
|
||||||
// Check if user has already accepted the disclaimer
|
// Check if user has already accepted the disclaimer
|
||||||
if (hasAcceptedDisclaimer()) {
|
if (hasAcceptedDisclaimer()) {
|
||||||
// Navigate directly without showing modal
|
// If there's a valid board path, navigate directly
|
||||||
const path = boardPath || address;
|
if (boardPath) {
|
||||||
navigate(`/${path}`);
|
navigate(`/${boardPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If no board path (placeholder), don't navigate - we'll handle this separately
|
||||||
|
// Don't show disclaimer modal either since it's already accepted
|
||||||
|
// Caller should handle showing directory modal or other UI
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface TopbarEditModalState {
|
||||||
|
showModal: boolean;
|
||||||
|
openTopbarEditModal: () => void;
|
||||||
|
closeTopbarEditModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTopbarEditModalStore = create<TopbarEditModalState>((set) => ({
|
||||||
|
showModal: false,
|
||||||
|
|
||||||
|
openTopbarEditModal: () => {
|
||||||
|
set({
|
||||||
|
showModal: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeTopbarEditModal: () => {
|
||||||
|
set({
|
||||||
|
showModal: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useTopbarEditModalStore;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { getAllBoardCodes } from '../constants/board-codes';
|
||||||
|
|
||||||
|
const LOCALSTORAGE_KEY_DIRECTORIES = '5chan-topbar-directories-visible';
|
||||||
|
const LOCALSTORAGE_KEY_SUBSCRIPTIONS = '5chan-topbar-subscriptions-visible';
|
||||||
|
|
||||||
|
interface TopbarVisibilityState {
|
||||||
|
// Directory codes that are visible (all visible by default)
|
||||||
|
visibleDirectories: Set<string>;
|
||||||
|
// Subscription addresses that are visible in topbar (all hidden by default)
|
||||||
|
visibleSubscriptions: Set<string>;
|
||||||
|
// Actions
|
||||||
|
toggleDirectory: (code: string) => void;
|
||||||
|
toggleSubscription: (address: string) => void;
|
||||||
|
setDirectoryVisibility: (code: string, visible: boolean) => void;
|
||||||
|
setSubscriptionVisibility: (address: string, visible: boolean) => void;
|
||||||
|
// Initialize from localStorage
|
||||||
|
initialize: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadFromLocalStorage = (key: string, defaultValue: Set<string>): Set<string> => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
if (stored) {
|
||||||
|
const array = JSON.parse(stored);
|
||||||
|
return new Set(array);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load ${key} from localStorage:`, e);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveToLocalStorage = (key: string, set: Set<string>) => {
|
||||||
|
try {
|
||||||
|
const array = Array.from(set);
|
||||||
|
localStorage.setItem(key, JSON.stringify(array));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to save ${key} to localStorage:`, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, get) => {
|
||||||
|
// Initialize with all directories visible by default
|
||||||
|
const allBoardCodes = getAllBoardCodes();
|
||||||
|
const defaultVisibleDirectories = new Set(allBoardCodes);
|
||||||
|
const defaultVisibleSubscriptions = new Set<string>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories),
|
||||||
|
visibleSubscriptions: loadFromLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, defaultVisibleSubscriptions),
|
||||||
|
|
||||||
|
toggleDirectory: (code: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const newSet = new Set(state.visibleDirectories);
|
||||||
|
if (newSet.has(code)) {
|
||||||
|
newSet.delete(code);
|
||||||
|
} else {
|
||||||
|
newSet.add(code);
|
||||||
|
}
|
||||||
|
saveToLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, newSet);
|
||||||
|
return { visibleDirectories: newSet };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSubscription: (address: string) => {
|
||||||
|
set((state) => {
|
||||||
|
const newSet = new Set(state.visibleSubscriptions);
|
||||||
|
if (newSet.has(address)) {
|
||||||
|
newSet.delete(address);
|
||||||
|
} else {
|
||||||
|
newSet.add(address);
|
||||||
|
}
|
||||||
|
saveToLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, newSet);
|
||||||
|
return { visibleSubscriptions: newSet };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setDirectoryVisibility: (code: string, visible: boolean) => {
|
||||||
|
set((state) => {
|
||||||
|
const newSet = new Set(state.visibleDirectories);
|
||||||
|
if (visible) {
|
||||||
|
newSet.add(code);
|
||||||
|
} else {
|
||||||
|
newSet.delete(code);
|
||||||
|
}
|
||||||
|
saveToLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, newSet);
|
||||||
|
return { visibleDirectories: newSet };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setSubscriptionVisibility: (address: string, visible: boolean) => {
|
||||||
|
set((state) => {
|
||||||
|
const newSet = new Set(state.visibleSubscriptions);
|
||||||
|
if (visible) {
|
||||||
|
newSet.add(address);
|
||||||
|
} else {
|
||||||
|
newSet.delete(address);
|
||||||
|
}
|
||||||
|
saveToLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, newSet);
|
||||||
|
return { visibleSubscriptions: newSet };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: () => {
|
||||||
|
// Load from localStorage on initialization
|
||||||
|
const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories);
|
||||||
|
const subscriptions = loadFromLocalStorage(LOCALSTORAGE_KEY_SUBSCRIPTIONS, defaultVisibleSubscriptions);
|
||||||
|
set({
|
||||||
|
visibleDirectories: directories,
|
||||||
|
visibleSubscriptions: subscriptions,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useTopbarVisibilityStore;
|
||||||
+41
-4
@@ -72,7 +72,13 @@
|
|||||||
--homepage-box-link-text-color-hover: #e00;
|
--homepage-box-link-text-color-hover: #e00;
|
||||||
--homepage-box-link-text-decoration: none;
|
--homepage-box-link-text-decoration: none;
|
||||||
--homepage-box-link-text-decoration-hover: underline;
|
--homepage-box-link-text-decoration-hover: underline;
|
||||||
--disclaimer-modal-close-button-background-image: url("/assets/buttons/icon-close-red.png");
|
--directory-modal-background-color: rgb(255, 255, 255);
|
||||||
|
--directory-modal-border: 1px solid rgb(136, 0, 0);
|
||||||
|
--directory-modal-text-color: rgb(136, 0, 0);
|
||||||
|
--directory-modal-header-background-color: rgb(136, 0, 0);
|
||||||
|
--directory-modal-header-text-color: rgb(255, 255, 255);
|
||||||
|
--directory-modal-close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||||
|
|
||||||
|
|
||||||
/* footer */
|
/* footer */
|
||||||
--footer-item-background-color-desktop: #fed;
|
--footer-item-background-color-desktop: #fed;
|
||||||
@@ -285,7 +291,12 @@
|
|||||||
--homepage-box-link-text-color-hover: #d00;
|
--homepage-box-link-text-color-hover: #d00;
|
||||||
--homepage-box-link-text-decoration: none;
|
--homepage-box-link-text-decoration: none;
|
||||||
--homepage-box-link-text-decoration-hover: underline;
|
--homepage-box-link-text-decoration-hover: underline;
|
||||||
--disclaimer-modal-close-button-background-image: url("/assets/buttons/icon-close-red.png");
|
--directory-modal-background-color: #d6daf0;
|
||||||
|
--directory-modal-border: 1px solid #b7c5d9;
|
||||||
|
--directory-modal-text-color: #000;
|
||||||
|
--directory-modal-header-background-color: #98e;
|
||||||
|
--directory-modal-header-text-color: #000;
|
||||||
|
--directory-modal-close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||||
|
|
||||||
/* horizontal rule */
|
/* horizontal rule */
|
||||||
--hr-border: none;
|
--hr-border: none;
|
||||||
@@ -485,6 +496,14 @@
|
|||||||
--close-button-background-image: url("/assets/buttons/cross-red.png");
|
--close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||||
--open-help-button-background-image: url("/assets/buttons/help-red.png");
|
--open-help-button-background-image: url("/assets/buttons/help-red.png");
|
||||||
|
|
||||||
|
/* directory modal */
|
||||||
|
--directory-modal-background-color: #f0e0d6;
|
||||||
|
--directory-modal-border: 1px solid rgba(0, 0, 0, 0.20);
|
||||||
|
--directory-modal-text-color: #800;
|
||||||
|
--directory-modal-header-background-color: #ea8;
|
||||||
|
--directory-modal-header-text-color: #800;
|
||||||
|
--directory-modal-close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||||
|
|
||||||
/* footer */
|
/* footer */
|
||||||
--footer-item-background-color-desktop: #fed;
|
--footer-item-background-color-desktop: #fed;
|
||||||
|
|
||||||
@@ -675,7 +694,12 @@
|
|||||||
--homepage-box-bar-text-color: #000;
|
--homepage-box-bar-text-color: #000;
|
||||||
--homepage-box-bar-background-color: #98e;
|
--homepage-box-bar-background-color: #98e;
|
||||||
--homepage-box-bar-text-color-hover: #fff;
|
--homepage-box-bar-text-color-hover: #fff;
|
||||||
--disclaimer-modal-close-button-background-image: url("/assets/buttons/icon-close-red.png");
|
--directory-modal-background-color: #d6daf0;
|
||||||
|
--directory-modal-border: 1px solid rgba(0, 0, 0, 0.20);
|
||||||
|
--directory-modal-text-color: #000;
|
||||||
|
--directory-modal-header-background-color: #98e;
|
||||||
|
--directory-modal-header-text-color: #000;
|
||||||
|
--directory-modal-close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||||
|
|
||||||
/* media */
|
/* media */
|
||||||
--media-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
--media-thumbnail-background-color: rgba(0, 0, 0, 0.05);
|
||||||
@@ -867,7 +891,12 @@
|
|||||||
--homepage-box-border-color: rgb(45, 47, 51);
|
--homepage-box-border-color: rgb(45, 47, 51);
|
||||||
--homepage-box-bar-text-color: #c5c8c6;
|
--homepage-box-bar-text-color: #c5c8c6;
|
||||||
--homepage-box-bar-text-color-hover: #c5c8c6;
|
--homepage-box-bar-text-color-hover: #c5c8c6;
|
||||||
--disclaimer-modal-close-button-background-image: url("/assets/buttons/icon-close-red.png");
|
--directory-modal-background-color: #282a2e;
|
||||||
|
--directory-modal-border: 1px solid #111;
|
||||||
|
--directory-modal-text-color: #c5c8c6;
|
||||||
|
--directory-modal-header-background-color: #282a2e;
|
||||||
|
--directory-modal-header-text-color: #c5c8c6;
|
||||||
|
--directory-modal-close-button-background-image: url("/assets/buttons/cross-dark.png");
|
||||||
|
|
||||||
/* horizontal rule */
|
/* horizontal rule */
|
||||||
--hr-border: none;
|
--hr-border: none;
|
||||||
@@ -1207,6 +1236,14 @@
|
|||||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-photon.png");
|
--settings-modal-show-button-background-image: url("/assets/buttons/plus-photon.png");
|
||||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-photon.png");
|
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-photon.png");
|
||||||
|
|
||||||
|
/* directory modal */
|
||||||
|
--directory-modal-background-color: #ddd;
|
||||||
|
--directory-modal-border: 1px solid #ccc;
|
||||||
|
--directory-modal-text-color: #333;
|
||||||
|
--directory-modal-header-background-color: #ddd;
|
||||||
|
--directory-modal-header-text-color: #333;
|
||||||
|
--directory-modal-close-button-background-image: url("/assets/buttons/cross-photon.png");
|
||||||
|
|
||||||
/* stats */
|
/* stats */
|
||||||
--stats-font-size: 11px;
|
--stats-font-size: 11px;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user