mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat: add christmas theme
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
+13
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useAccountComment, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
import { useAccountComment, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import { initSnow, removeSnow, shouldShowSnow } from './lib/snow';
|
||||||
import { isAllView, isSubscriptionsView } from './lib/utils/view-utils';
|
import { isAllView, isSubscriptionsView } from './lib/utils/view-utils';
|
||||||
import useIsMobile from './hooks/use-is-mobile';
|
import useIsMobile from './hooks/use-is-mobile';
|
||||||
import useTheme from './hooks/use-theme';
|
import useTheme from './hooks/use-theme';
|
||||||
@@ -19,6 +20,7 @@ import ChallengeModal from './components/challenge-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 useSpecialThemeStore from './stores/use-special-theme-store';
|
||||||
|
|
||||||
const ValidateRouteParams = () => {
|
const ValidateRouteParams = () => {
|
||||||
const { accountCommentIndex, timeFilterName } = useParams();
|
const { accountCommentIndex, timeFilterName } = useParams();
|
||||||
@@ -51,6 +53,17 @@ const BoardLayout = () => {
|
|||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||||
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
|
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
|
||||||
|
|
||||||
|
// Christmas theme
|
||||||
|
const { isEnabled: isSpecialEnabled } = useSpecialThemeStore();
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSpecialEnabled) {
|
||||||
|
initSnow({ flakeCount: 150 });
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
removeSnow();
|
||||||
|
};
|
||||||
|
}, [isSpecialEnabled]);
|
||||||
|
|
||||||
// force rerender of post form when navigating between pages, except when opening settings modal in current view
|
// force rerender of post form when navigating between pages, except when opening settings modal in current view
|
||||||
const key = location.pathname.endsWith('/settings')
|
const key = location.pathname.endsWith('/settings')
|
||||||
? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}`
|
? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}`
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.boardTitle {
|
.content {
|
||||||
padding-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,3 +49,13 @@
|
|||||||
padding: 5px 0 2px 0;
|
padding: 5px 0 2px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.garland {
|
||||||
|
border-image-slice: 50 0 50 0;
|
||||||
|
border-image-width: 40px 0px 0px 0px;
|
||||||
|
border-image-outset: 0px 0px 0px 0px;
|
||||||
|
border-image-repeat: repeat repeat;
|
||||||
|
border-image-source: url('/public/assets/garland.png');
|
||||||
|
border-style: solid;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import styles from './board-header.module.css';
|
|||||||
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
|
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||||
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
|
|
||||||
const totalBanners = 61;
|
const totalBanners = 61;
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ const BoardHeader = () => {
|
|||||||
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
|
||||||
{!useIsMobile() && (
|
{!useIsMobile() && (
|
||||||
<div className={styles.bannerCnt}>
|
<div className={styles.bannerCnt}>
|
||||||
<ImageBanner key={isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : address} />
|
<ImageBanner key={isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : address} />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import Tooltip from '../tooltip';
|
|||||||
import { PostProps } from '../../views/post/post';
|
import { PostProps } from '../../views/post/post';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
|
|
||||||
interface ShowOmittedRepliesState {
|
interface ShowOmittedRepliesState {
|
||||||
showOmittedReplies: Record<string, boolean>;
|
showOmittedReplies: Record<string, boolean>;
|
||||||
@@ -213,15 +214,15 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }:
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostMedia = ({ post }: PostProps) => {
|
const PostMedia = ({ post, hasThumbnail }: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { link, spoiler, cid } = post || {};
|
const { link, spoiler, cid } = post || {};
|
||||||
|
|
||||||
// Reset state by remounting component when post changes
|
// Reset state by remounting component when post changes
|
||||||
return <PostMediaContent key={cid} post={post} link={link} spoiler={spoiler} t={t} />;
|
return <PostMediaContent key={cid} post={post} hasThumbnail={hasThumbnail} spoiler={spoiler} t={t} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostMediaContent = ({ post, link, spoiler, t }: { post: any; link: string; spoiler: boolean; t: any }) => {
|
const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasThumbnail: boolean | undefined; spoiler: boolean; t: any }) => {
|
||||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||||
const commentMediaInfo = useCommentMediaInfo(post);
|
const commentMediaInfo = useCommentMediaInfo(post);
|
||||||
const { url } = commentMediaInfo || {};
|
const { url } = commentMediaInfo || {};
|
||||||
@@ -235,7 +236,6 @@ const PostMediaContent = ({ post, link, spoiler, t }: { post: any; link: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const embedUrl = url && new URL(url);
|
const embedUrl = url && new URL(url);
|
||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
|
||||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||||
|
|
||||||
const mediaDimensions = getMediaDimensions(commentMediaInfo);
|
const mediaDimensions = getMediaDimensions(commentMediaInfo);
|
||||||
@@ -451,6 +451,9 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
|||||||
replyCount: 0,
|
replyCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commentMediaInfo = useCommentMediaInfo(post);
|
||||||
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.postDesktop}>
|
<div className={styles.postDesktop}>
|
||||||
{showReplies ? (
|
{showReplies ? (
|
||||||
@@ -466,8 +469,9 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
|||||||
<span className={`${styles.hideButton} ${hidden ? styles.unhideThread : styles.hideThread}`} onClick={hidden ? unhide : hide} />
|
<span className={`${styles.hideButton} ${hidden ? styles.unhideThread : styles.hideThread}`} onClick={hidden ? unhide : hide} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
<div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid} className={shouldShowSnow() && hasThumbnail ? styles.xmasHatWrapper : ''}>
|
||||||
{link && !isHidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} />}
|
{shouldShowSnow() && hasThumbnail && <img src={`${process.env.PUBLIC_URL}/assets/xmashat.gif`} className={styles.xmasHat} />}
|
||||||
|
{link && !isHidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} hasThumbnail={hasThumbnail} />}
|
||||||
<PostInfo isHidden={hidden} openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
|
<PostInfo isHidden={hidden} openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
|
||||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||||
{!isHidden && <PostMessage post={post} />}
|
{!isHidden && <PostMessage post={post} />}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import ReplyQuotePreview from '../reply-quote-preview';
|
|||||||
import Tooltip from '../tooltip';
|
import Tooltip from '../tooltip';
|
||||||
import { PostProps } from '../../views/post/post';
|
import { PostProps } from '../../views/post/post';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
|
|
||||||
const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: PostProps) => {
|
const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: PostProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -400,7 +401,13 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
|||||||
)}
|
)}
|
||||||
<div className={showReplies ? styles.thread : styles.quotePreview}>
|
<div className={showReplies ? styles.thread : styles.quotePreview}>
|
||||||
<div className={styles.postContainer}>
|
<div className={styles.postContainer}>
|
||||||
<div className={styles.postOp} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
<div
|
||||||
|
className={`${styles.postOp} ${shouldShowSnow() ? styles.xmasHatWrapper : ''}`}
|
||||||
|
data-cid={cid}
|
||||||
|
data-author-address={author?.shortAddress}
|
||||||
|
data-post-cid={postCid}
|
||||||
|
>
|
||||||
|
{shouldShowSnow() && <img src={`${process.env.PUBLIC_URL}/assets/xmashat.gif`} className={styles.xmasHat} />}
|
||||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
|
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
|
||||||
<PostMessageMobile post={post} />
|
<PostMessageMobile post={post} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import _ from 'lodash';
|
|||||||
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
||||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||||
import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
|
import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
|
||||||
|
import useSpecialThemeStore from '../../../stores/use-special-theme-store';
|
||||||
|
|
||||||
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
||||||
const isElectron = window.isElectron === true;
|
const isElectron = window.isElectron === true;
|
||||||
@@ -69,19 +70,33 @@ const CheckForUpdates = () => {
|
|||||||
|
|
||||||
const Style = () => {
|
const Style = () => {
|
||||||
const [theme, setTheme] = useTheme();
|
const [theme, setTheme] = useTheme();
|
||||||
|
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
|
||||||
|
const today = new Date();
|
||||||
|
const month = today.getMonth();
|
||||||
|
const day = today.getDate();
|
||||||
|
const isChristmas = month === 11 && (day === 24 || day === 25);
|
||||||
|
|
||||||
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
setTheme(e.target.value);
|
const newTheme = e.target.value;
|
||||||
|
|
||||||
|
if (newTheme === 'special') {
|
||||||
|
setIsEnabled(true);
|
||||||
|
setTheme('tomorrow');
|
||||||
|
} else {
|
||||||
|
setIsEnabled(false);
|
||||||
|
setTheme(newTheme);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select className={styles.themeSettings} value={theme} onChange={handleThemeChange}>
|
<select className={styles.themeSettings} value={isEnabled ? 'special' : theme} onChange={handleThemeChange}>
|
||||||
<option value='yotsuba'>Yotsuba</option>
|
<option value='yotsuba'>Yotsuba</option>
|
||||||
<option value='yotsuba-b'>Yotsuba B</option>
|
<option value='yotsuba-b'>Yotsuba B</option>
|
||||||
<option value='futaba'>Futaba</option>
|
<option value='futaba'>Futaba</option>
|
||||||
<option value='burichan'>Burichan</option>
|
<option value='burichan'>Burichan</option>
|
||||||
<option value='tomorrow'>Tomorrow</option>
|
<option value='tomorrow'>Tomorrow</option>
|
||||||
<option value='photon'>Photon</option>
|
<option value='photon'>Photon</option>
|
||||||
|
{isChristmas && <option value='special'>Special</option>}
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import useDefaultSubplebbits from './use-default-subplebbits';
|
|||||||
import useInitialTheme from './use-initial-theme';
|
import useInitialTheme from './use-initial-theme';
|
||||||
import { nsfwTags } from '../views/home/home';
|
import { nsfwTags } from '../views/home/home';
|
||||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||||
|
|
||||||
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'];
|
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'];
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
|||||||
const pendingPostCommentIndex = pendingPostParams?.accountCommentIndex ? parseInt(pendingPostParams.accountCommentIndex) : undefined;
|
const pendingPostCommentIndex = pendingPostParams?.accountCommentIndex ? parseInt(pendingPostParams.accountCommentIndex) : undefined;
|
||||||
const pendingPost = useAccountComment({ commentIndex: pendingPostCommentIndex });
|
const pendingPost = useAccountComment({ commentIndex: pendingPostCommentIndex });
|
||||||
const pendingPostSubplebbitAddress = pendingPost?.subplebbitAddress;
|
const pendingPostSubplebbitAddress = pendingPost?.subplebbitAddress;
|
||||||
|
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
|
||||||
|
|
||||||
const setThemeStore = useThemeStore((state) => state.setTheme);
|
const setThemeStore = useThemeStore((state) => state.setTheme);
|
||||||
const getTheme = useThemeStore((state) => state.getTheme);
|
const getTheme = useThemeStore((state) => state.getTheme);
|
||||||
@@ -32,11 +34,40 @@ const useTheme = (): [string, (theme: string) => void] => {
|
|||||||
const initialTheme = useInitialTheme(pendingPostSubplebbitAddress);
|
const initialTheme = useInitialTheme(pendingPostSubplebbitAddress);
|
||||||
const [currentTheme, setCurrentTheme] = useState(initialTheme);
|
const [currentTheme, setCurrentTheme] = useState(initialTheme);
|
||||||
|
|
||||||
|
const isInAllView = isAllView(location.pathname);
|
||||||
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||||
|
|
||||||
|
// Check for Christmas and initialize special theme if needed
|
||||||
|
useEffect(() => {
|
||||||
|
const today = new Date();
|
||||||
|
const month = today.getMonth();
|
||||||
|
const day = today.getDate();
|
||||||
|
const isChristmas = month === 11 && (day === 24 || day === 25);
|
||||||
|
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||||
|
|
||||||
|
if (isChristmas && isEnabled === null && subplebbitAddress && !isInAllView && !isInSubscriptionsView) {
|
||||||
|
setIsEnabled(true);
|
||||||
|
setCurrentTheme('tomorrow');
|
||||||
|
updateThemeClass('tomorrow');
|
||||||
|
}
|
||||||
|
}, [isEnabled, setIsEnabled, params, pendingPostSubplebbitAddress, location.pathname]);
|
||||||
|
|
||||||
const getCurrentTheme = useCallback(() => {
|
const getCurrentTheme = useCallback(() => {
|
||||||
|
const { isEnabled } = useSpecialThemeStore.getState();
|
||||||
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||||
const isInAllView = isAllView(location.pathname);
|
const isInAllView = isAllView(location.pathname);
|
||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||||
|
|
||||||
|
// Always use yotsuba for home page
|
||||||
|
if (location.pathname === '/') {
|
||||||
|
return 'yotsuba';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If special theme is enabled, use tomorrow
|
||||||
|
if (isEnabled) {
|
||||||
|
return 'tomorrow';
|
||||||
|
}
|
||||||
|
|
||||||
let storedTheme = null;
|
let storedTheme = null;
|
||||||
if (isInAllView || isInSubscriptionsView) {
|
if (isInAllView || isInSubscriptionsView) {
|
||||||
storedTheme = getTheme('sfw', false);
|
storedTheme = getTheme('sfw', false);
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||||
|
|
||||||
|
interface SnowOptions {
|
||||||
|
flakeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this new variable to track manual override
|
||||||
|
let manualSnowOverride: boolean | null = null;
|
||||||
|
|
||||||
|
export const setManualSnowOverride = (value: boolean): void => {
|
||||||
|
manualSnowOverride = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initSnow = ({ flakeCount }: SnowOptions): void => {
|
||||||
|
const randomRange = (min: number, max: number): number => {
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
const snowflakeName = 'js-snowflake';
|
||||||
|
|
||||||
|
let rule = `.${snowflakeName} {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: linear-gradient(white, white);
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: drop-shadow(0 0 10px white);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
for (let i = 1; i < flakeCount; i++) {
|
||||||
|
const randomX = Math.random() * 100;
|
||||||
|
const randomOffset = randomRange(-100000, 100000) * 0.0001;
|
||||||
|
const randomXEnd = randomX + randomOffset;
|
||||||
|
const randomXEndYoyo = randomX + randomOffset / 2;
|
||||||
|
const randomYoyoTime = randomRange(30000, 80000) / 100000;
|
||||||
|
const randomYoyoY = randomYoyoTime * 100;
|
||||||
|
const randomScale = Math.random() * (1.0 - 0.2) + 0.2;
|
||||||
|
const fallDuration = randomRange(10, 30);
|
||||||
|
const fallDelay = (Math.floor(Math.random() * 30) + 1) * -1;
|
||||||
|
const alpha = Math.random() * (1.0 - 0.1) + 0.1;
|
||||||
|
|
||||||
|
rule += `
|
||||||
|
.${snowflakeName}:nth-child(${i}) {
|
||||||
|
opacity: ${alpha};
|
||||||
|
transform: translate(${randomX}vw, -10px) scale(${randomScale});
|
||||||
|
animation: fall-${i} ${fallDuration}s ${fallDelay}s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fall-${i} {
|
||||||
|
${randomYoyoTime * 100}% {
|
||||||
|
transform: translate(${randomXEnd}vw, ${randomYoyoY}vh) scale(${randomScale});
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate(${randomXEndYoyo}vw, 100vh) scale(${randomScale});
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'js-snowfield';
|
||||||
|
container.style.position = 'fixed';
|
||||||
|
container.style.top = '0';
|
||||||
|
container.style.pointerEvents = 'none';
|
||||||
|
container.style.zIndex = '9999';
|
||||||
|
|
||||||
|
for (let i = 1; i < flakeCount; i++) {
|
||||||
|
const flake = document.createElement('div');
|
||||||
|
flake.className = snowflakeName;
|
||||||
|
container.appendChild(flake);
|
||||||
|
}
|
||||||
|
|
||||||
|
const css = document.createElement('style');
|
||||||
|
css.type = 'text/css';
|
||||||
|
css.textContent = rule;
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(css);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeSnow = (): void => {
|
||||||
|
const element = document.getElementById('js-snowfield');
|
||||||
|
if (element) {
|
||||||
|
element.parentNode?.removeChild(element);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldShowSnow = (): boolean => {
|
||||||
|
const isEnabled = useSpecialThemeStore.getState().isEnabled;
|
||||||
|
|
||||||
|
// Check store value first
|
||||||
|
if (isEnabled !== null) {
|
||||||
|
return isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check manual override second
|
||||||
|
if (manualSnowOverride !== null) {
|
||||||
|
return manualSnowOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const month = today.getMonth();
|
||||||
|
const day = today.getDate();
|
||||||
|
return month === 11 && (day === 24 || day === 25);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
interface SpecialThemeStore {
|
||||||
|
isEnabled: boolean | null;
|
||||||
|
setIsEnabled: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSpecialThemeStore = create(
|
||||||
|
persist<SpecialThemeStore>(
|
||||||
|
(set) => ({
|
||||||
|
isEnabled: null,
|
||||||
|
setIsEnabled: (value: boolean) => set({ isEnabled: value }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'Special-theme-storage',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default useSpecialThemeStore;
|
||||||
@@ -38,3 +38,13 @@
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.garland {
|
||||||
|
border-image-slice: 50 0 50 0;
|
||||||
|
border-image-width: 40px 0px 0px 0px;
|
||||||
|
border-image-outset: 0px 0px 0px 0px;
|
||||||
|
border-image-repeat: repeat repeat;
|
||||||
|
border-image-source: url('/public/assets/garland.png');
|
||||||
|
border-style: solid;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import SubplebbitDescription from '../../components/subplebbit-description';
|
|||||||
import SubplebbitRules from '../../components/subplebbit-rules';
|
import SubplebbitRules from '../../components/subplebbit-rules';
|
||||||
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
||||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||||
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
|
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
@@ -262,7 +263,9 @@ const Board = () => {
|
|||||||
}, [title, shortAddress, subplebbitAddress]);
|
}, [title, shortAddress, subplebbitAddress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<>
|
||||||
|
{shouldShowSnow() && <hr />}
|
||||||
|
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
|
||||||
{location.pathname.endsWith('/settings') && <SettingsModal />}
|
{location.pathname.endsWith('/settings') && <SettingsModal />}
|
||||||
{activeCid && threadCid && postSubplebbitAddress && (
|
{activeCid && threadCid && postSubplebbitAddress && (
|
||||||
<ReplyModal
|
<ReplyModal
|
||||||
@@ -304,6 +307,7 @@ const Board = () => {
|
|||||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,30 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xmasHatWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xmasHat {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: -80px;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.xmasHat {
|
||||||
|
width: 100px;
|
||||||
|
top: -20px;
|
||||||
|
left: -15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.xmasHat img {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* clearfix to contain float elements */
|
/* clearfix to contain float elements */
|
||||||
.postDesktop::after {
|
.postDesktop::after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import styles from './post.module.css';
|
|||||||
export interface PostProps {
|
export interface PostProps {
|
||||||
index?: number;
|
index?: number;
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
|
hasThumbnail?: boolean;
|
||||||
post?: any;
|
post?: any;
|
||||||
postReplyCount?: number;
|
postReplyCount?: number;
|
||||||
reply?: any;
|
reply?: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user