feat(subplebbit): add automatic theme based on nsfw or sfw tags

This commit is contained in:
Tom (plebeius.eth)
2024-06-17 12:17:36 +02:00
parent 77ccf0c849
commit 22c1acdd3d
20 changed files with 189 additions and 87 deletions
+37 -15
View File
@@ -1,8 +1,6 @@
import { useEffect } from 'react';
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { isAllView, isHomeView, isSubscriptionsView } from './lib/utils/view-utils';
import { isAllView, isHomeView, isNotFoundView, isSubscriptionsView } from './lib/utils/view-utils';
import useIsMobile from './hooks/use-is-mobile';
import useTheme from './hooks/use-theme';
import styles from './app.module.css';
import Board from './views/board';
import Catalog from './views/catalog';
@@ -16,17 +14,22 @@ import ChallengeModal from './components/challenge-modal';
import PostForm from './components/post-form';
import SubplebbitStats from './components/subplebbit-stats';
import TopBar from './components/topbar';
import { useEffect } from 'react';
import { nsfwTags } from './views/home/home';
import useDefaultSubplebbits from './hooks/use-default-subplebbits';
import useThemeStore from './stores/use-theme-store';
import { timeFilterNames } from './hooks/use-time-filter';
const BoardLayout = () => {
const { accountCommentIndex, subplebbitAddress } = useParams();
const { accountCommentIndex, subplebbitAddress, timeFilterName } = useParams();
const location = useLocation();
const isMobile = useIsMobile();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isValidAccountCommentIndex = !accountCommentIndex || (!isNaN(parseInt(accountCommentIndex)) && parseInt(accountCommentIndex) >= 0);
if (!isValidAccountCommentIndex) {
if (!isValidAccountCommentIndex || (timeFilterName && !timeFilterNames.includes(timeFilterName))) {
return <NotFound />;
}
@@ -56,28 +59,47 @@ const BoardLayout = () => {
);
};
const App = () => {
const GlobalLayout = () => {
const location = useLocation();
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
const getTheme = useThemeStore((state) => state.getTheme);
const setTheme = useThemeStore((state) => state.setTheme);
const subplebbits = useDefaultSubplebbits();
const isInHomeView = isHomeView(location.pathname);
const [theme] = useTheme();
const isInNotFoundView = isNotFoundView(location.pathname, useParams());
useEffect(() => {
document.body.classList.forEach((className) => document.body.classList.remove(className));
const classToAdd = isInHomeView ? 'yotsuba' : theme;
document.body.classList.add(classToAdd);
}, [theme, isInHomeView]);
let theme = 'yotsuba-b';
const globalLayout = (
if (isInHomeView || isInNotFoundView) {
theme = 'yotsuba';
} else if (subplebbitAddress) {
theme = getTheme(subplebbitAddress);
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag)) && theme === 'yotsuba-b') {
theme = 'yotsuba';
setTheme(subplebbitAddress, 'yotsuba');
}
}
document.body.classList.remove('yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon');
document.body.classList.add(theme);
}, [location.pathname, subplebbitAddress, getTheme, setTheme, subplebbits, isInHomeView]);
return (
<>
<ChallengeModal />
<Outlet />
</>
);
};
const App = () => {
return (
<div className={styles.app}>
<Routes>
<Route element={globalLayout}>
<Route element={<GlobalLayout />}>
<Route path='/' element={<Home />} />
<Route element={<BoardLayout />}>
<Route path='/p/:subplebbitAddress' element={<Board />} />
@@ -144,11 +144,11 @@ export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView
export const MobileBoardButtons = () => {
const params = useParams();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
const isInPendingPostPage = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
@@ -190,10 +190,10 @@ export const DesktopBoardButtons = () => {
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
const isInCatalogView = isCatalogView(location.pathname, params);
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInPendingPostPage = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
return (
<>
+2 -2
View File
@@ -20,8 +20,8 @@ const BoardHeader = () => {
const location = useLocation();
const params = useParams();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Link, useLocation } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom';
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
@@ -106,7 +106,7 @@ const CatalogPost = ({ post }: { post: Comment }) => {
const { hidden } = useHide({ cid });
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const postLink = isInAllView && isDescription ? `/p/all/description` : `/p/${subplebbitAddress}/${isDescription ? 'description' : isRules ? 'rules' : `c/${cid}`}`;
+2 -2
View File
@@ -34,9 +34,9 @@ const PostInfo = ({ openReplyModal, post, roles, isHidden }: PostProps) => {
const params = useParams();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const shortDisplayName = displayName?.trim().length > 20 ? displayName?.trim().slice(0, 20).trim() + '...' : displayName?.trim();
const authorRole = roles?.[address]?.role;
@@ -134,10 +134,10 @@ const PostMenuDesktop = ({ post }: { post: Comment }) => {
const location = useLocation();
const params = useParams();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
const isInPostPageView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const { refs, floatingStyles, context } = useFloating({
placement: 'bottom-start',
+4 -4
View File
@@ -77,8 +77,8 @@ const PostFormTable = ({ closeForm }: { closeForm: () => void }) => {
const subjectRef = useRef<HTMLInputElement>(null);
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subscriptions = account?.subscriptions || [];
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
@@ -262,8 +262,8 @@ const PostForm = () => {
const isInDescriptionView = isDescriptionView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const isInRulesView = isRulesView(location.pathname, params);
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const comment = useComment({ commentCid: useParams().commentCid });
const { deleted, locked, removed } = comment || {};
+3 -3
View File
@@ -26,8 +26,8 @@ const PostInfoAndMedia = ({ openReplyModal, post, roles }: PostProps) => {
const { address, displayName, shortAddress } = author || {};
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const authorRole = roles?.[address]?.role;
const shortDisplayName = displayName?.trim().length > 20 ? displayName?.trim().slice(0, 20).trim() + '...' : displayName?.trim();
@@ -196,7 +196,7 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
const { isDescription, isRules } = post || {}; // custom properties, not from api
const params = useParams();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInPendingPostView = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const linksCount = useCountLinksInReplies(post);
+3 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Draggable from 'react-draggable';
import { setAccount, useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { setAccount, useAccount, useAuthorAddress, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isValidURL } from '../../lib/utils/url-utils';
@@ -78,8 +78,8 @@ const ReplyModal = ({ closeModal, parentCid, scrollY }: ReplyModalProps) => {
}, [parentCid]);
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subplebbit = useSubplebbit({ subplebbitAddress });
const { updatedAt } = subplebbit || {};
const isBoardOffline = subplebbit?.updatedAt && subplebbit.updatedAt < Date.now() / 1000 - 60 * 60;
@@ -65,8 +65,12 @@ const CheckForUpdates = () => {
const Style = () => {
const [theme, setTheme] = useTheme();
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setTheme(e.target.value);
};
return (
<select className={styles.themeSettings} value={theme} onChange={(e) => setTheme(e.target.value)}>
<select className={styles.themeSettings} value={theme} onChange={handleThemeChange}>
<option value='yotsuba'>Yotsuba</option>
<option value='yotsuba-b'>Yotsuba B</option>
<option value='futaba'>Futaba</option>
@@ -2,7 +2,7 @@ import { Post } from '../../views/post';
import { useTranslation } from 'react-i18next';
import { isAllView } from '../../lib/utils/view-utils';
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
import { useLocation } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';
interface DescriptionPostProps {
avatarUrl?: string;
@@ -16,7 +16,7 @@ interface DescriptionPostProps {
const SubplebbitDescription = ({ avatarUrl, createdAt, description, shortAddress, subplebbitAddress, title }: DescriptionPostProps) => {
const { t } = useTranslation();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const multisubMetadata = useMultisubMetadata();
const post = {
+2 -2
View File
@@ -104,9 +104,9 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
const location = useLocation();
const params = useParams();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : subplebbitAddress;
const boardSelect = (
+43 -2
View File
@@ -1,8 +1,49 @@
import { useEffect, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import useThemeStore from '../stores/use-theme-store';
import useDefaultSubplebbits from './use-default-subplebbits';
import { isHomeView } from '../lib/utils/view-utils';
import { nsfwTags } from '../views/home/home';
const useTheme = (): [string, (theme: string) => void] => {
const { theme, setTheme } = useThemeStore();
return [theme, setTheme];
const location = useLocation();
const isInHomeView = isHomeView(location.pathname);
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
const getTheme = useThemeStore((state) => state.getTheme);
const setTheme = useThemeStore((state) => state.setTheme);
const subplebbits = useDefaultSubplebbits();
const [theme, setLocalTheme] = useState('yotsuba-b');
useEffect(() => {
let initialTheme = 'yotsuba-b';
if (isInHomeView) {
initialTheme = 'yotsuba';
} else if (subplebbitAddress) {
initialTheme = getTheme(subplebbitAddress);
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag)) && initialTheme === 'yotsuba-b') {
initialTheme = 'yotsuba';
setTheme(subplebbitAddress, 'yotsuba');
}
}
setLocalTheme(initialTheme);
document.body.classList.remove('yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon');
document.body.classList.add(initialTheme);
}, [subplebbitAddress, subplebbits, getTheme, setTheme, isInHomeView, location.pathname]);
const setSubplebbitTheme = (theme: string) => {
if (subplebbitAddress) {
setTheme(subplebbitAddress, theme);
setLocalTheme(theme);
document.body.classList.remove('yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon');
document.body.classList.add(theme);
}
};
return [theme, setSubplebbitTheme];
};
export default useTheme;
+1 -1
View File
@@ -40,7 +40,7 @@ if (secondsSinceLastVisit > 30 * day) {
timeFilterNamesToSeconds[lastVisitTimeFilterName] = timeFilterNamesToSeconds['24h'];
}
const timeFilterNames = [lastVisitTimeFilterName, '1h', '12h', '24h', '48h', 'week', 'month', 'year', 'all'];
export const timeFilterNames = [lastVisitTimeFilterName, '1h', '12h', '24h', '48h', 'week', 'month', 'year', 'all'];
const useTimeFilter = () => {
const params = useParams();
+37 -6
View File
@@ -1,3 +1,5 @@
import { timeFilterNames } from '../../hooks/use-time-filter';
export type ParamsType = {
accountCommentIndex?: string;
commentCid?: string;
@@ -5,8 +7,25 @@ export type ParamsType = {
timeFilterName?: string;
};
export const isAllView = (pathname: string): boolean => {
return pathname.startsWith('/p/all');
export const isAllView = (pathname: string, params: ParamsType): boolean => {
const { timeFilterName } = params;
if (timeFilterName && !timeFilterNames.includes(timeFilterName)) {
return false;
}
return (
pathname === '/p/all' ||
pathname === '/p/all/settings' ||
pathname === `/p/all/${timeFilterName}` ||
pathname === `/p/all/${timeFilterName}/settings` ||
pathname === '/p/all/catalog' ||
pathname === '/p/all/catalog/settings' ||
pathname === `/p/all/catalog/${timeFilterName}` ||
pathname === `/p/all/catalog/${timeFilterName}/settings` ||
pathname === '/p/all/description' ||
pathname === '/p/all/description/settings'
);
};
export const isBoardView = (pathname: string, params: ParamsType): boolean => {
@@ -68,19 +87,31 @@ export const isSettingsView = (pathname: string, params: ParamsType): boolean =>
);
};
export const isSubscriptionsView = (pathname: string): boolean => {
return pathname.startsWith('/p/subscriptions');
export const isSubscriptionsView = (pathname: string, params: ParamsType): boolean => {
const { timeFilterName } = params;
return (
pathname === '/p/subscriptions' ||
pathname === '/p/subscriptions/settings' ||
pathname === `/p/subscriptions/${timeFilterName}` ||
pathname === `/p/subscriptions/${timeFilterName}/settings` ||
pathname === '/p/subscriptions/catalog' ||
pathname === '/p/subscriptions/catalog/settings' ||
pathname === `/p/subscriptions/catalog/${timeFilterName}` ||
pathname === `/p/subscriptions/catalog/${timeFilterName}/settings`
);
};
export const isNotFoundView = (pathname: string, params: ParamsType): boolean => {
return (
!isAllView(pathname) &&
!isAllView(pathname, params) &&
!isBoardView(pathname, params) &&
!isCatalogView(pathname, params) &&
!isDescriptionView(pathname, params) &&
!isHomeView(pathname) &&
!isPendingPostView(pathname, params) &&
!isPostPageView(pathname, params) &&
!isRulesView(pathname, params)
!isRulesView(pathname, params) &&
!isSettingsView(pathname, params) &&
!isSubscriptionsView(pathname, params)
);
};
+31 -7
View File
@@ -1,16 +1,40 @@
import { create, StoreApi } from 'zustand';
import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js';
interface ThemeState {
theme: string;
setTheme: (theme: string) => void;
themes: Record<string, string>;
setTheme: (subplebbitAddress: string, theme: string) => void;
getTheme: (subplebbitAddress: string) => string;
loadThemes: () => void;
}
const useThemeStore = create<ThemeState>((set: StoreApi<ThemeState>['setState']) => ({
theme: localStorage.getItem('theme') || 'yotsuba',
setTheme: (theme: string) => {
localStorage.setItem('theme', theme);
set({ theme });
const themeStore = localForageLru.createInstance({
name: 'themeStore',
size: 1000,
});
const useThemeStore = create<ThemeState>((set: StoreApi<ThemeState>['setState'], get: StoreApi<ThemeState>['getState']) => ({
themes: {},
setTheme: async (subplebbitAddress: string, theme: string) => {
const currentThemes = get().themes;
const updatedThemes = { ...currentThemes, [subplebbitAddress]: theme };
await themeStore.setItem(subplebbitAddress, theme);
set({ themes: updatedThemes });
},
getTheme: (subplebbitAddress: string) => {
const currentThemes = get().themes;
return currentThemes[subplebbitAddress] || 'yotsuba-b';
},
loadThemes: async () => {
const entries: [string, string][] = await themeStore.entries();
const themes: Record<string, string> = {};
entries.forEach(([key, value]) => {
themes[key] = value;
});
set({ themes });
},
}));
useThemeStore.getState().loadThemes();
export default useThemeStore;
+2 -2
View File
@@ -25,12 +25,12 @@ const Board = () => {
const location = useLocation();
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
const account = useAccount();
const subscriptions = account?.subscriptions;
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subplebbitAddresses = useMemo(() => {
if (isInAllView) {
@@ -9,8 +9,9 @@ const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
const { t } = useTranslation();
const { showAdultBoards, setShowAdultBoards, showGoreBoards, setShowGoreBoards, showTextOnlyThreads, setShowTextOnlyThreads } = useCatalogFiltersStore();
const location = useLocation();
const isInCatalogView = isCatalogView(location.pathname, useParams());
const isInAllView = isAllView(location.pathname);
const params = useParams();
const isInCatalogView = isCatalogView(location.pathname, params);
const isInAllView = isAllView(location.pathname, params);
return (
<>
+3 -7
View File
@@ -27,7 +27,7 @@ const useFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, subp
const { avatarUrl } = suggested || {};
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const multisub = useMultisubMetadata();
const feedWithDescriptionAndRules = useMemo(() => {
@@ -90,13 +90,13 @@ const Catalog = () => {
const location = useLocation();
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
const isInAllView = isAllView(location.pathname);
const isInAllView = isAllView(location.pathname, useParams());
const defaultSubplebbits = useDefaultSubplebbits();
const { showAdultBoards, showGoreBoards } = useCatalogFiltersStore();
const account = useAccount();
const subscriptions = account?.subscriptions;
const isInSubscriptionsView = isSubscriptionsView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subplebbitAddresses = useMemo(() => {
const filteredDefaultSubplebbits = defaultSubplebbits
@@ -121,10 +121,6 @@ const Catalog = () => {
return [subplebbitAddress];
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, showAdultBoards, showGoreBoards]);
useEffect(() => {
console.log(showAdultBoards, showGoreBoards);
}, [showAdultBoards, showGoreBoards]);
const columnCount = Math.floor(useWindowWidth() / columnWidth);
// postPerPage based on columnCount for optimized feed, dont change value after first render
// eslint-disable-next-line
+1 -18
View File
@@ -1,9 +1,8 @@
import { useEffect, useState, useRef } from 'react';
import { useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { HomeLogo } from '../home';
import styles from './not-found.module.css';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import useTheme from '../../hooks/use-theme';
const totalNotFoundImages = 2;
@@ -20,22 +19,6 @@ const NotFound = () => {
const { subplebbitAddress } = useParams();
const isValidSubplebbitAddress = !subplebbitAddress || subplebbitAddress.includes('.') || /^12D3K[a-zA-Z0-9]{44}$/.test(subplebbitAddress);
const [theme, setTheme] = useTheme();
const previousThemeRef = useRef(theme);
useEffect(() => {
if (theme !== 'yotsuba') {
previousThemeRef.current = theme;
setTheme('yotsuba');
}
return () => {
if (theme === 'yotsuba') {
setTheme(previousThemeRef.current);
}
};
}, [theme, setTheme]);
return (
<div className={styles.wrapper}>
<div className={styles.content}>