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:
+13
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAccountComment, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
import { initSnow, removeSnow, shouldShowSnow } from './lib/snow';
|
||||
import { isAllView, isSubscriptionsView } from './lib/utils/view-utils';
|
||||
import useIsMobile from './hooks/use-is-mobile';
|
||||
import useTheme from './hooks/use-theme';
|
||||
@@ -19,6 +20,7 @@ import ChallengeModal from './components/challenge-modal';
|
||||
import PostForm from './components/post-form';
|
||||
import SubplebbitStats from './components/subplebbit-stats';
|
||||
import TopBar from './components/topbar';
|
||||
import useSpecialThemeStore from './stores/use-special-theme-store';
|
||||
|
||||
const ValidateRouteParams = () => {
|
||||
const { accountCommentIndex, timeFilterName } = useParams();
|
||||
@@ -51,6 +53,17 @@ const BoardLayout = () => {
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
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
|
||||
const key = location.pathname.endsWith('/settings')
|
||||
? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}`
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.boardTitle {
|
||||
padding-top: 40px;
|
||||
.content {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,14 @@
|
||||
.bannerCnt {
|
||||
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 useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
|
||||
const totalBanners = 61;
|
||||
|
||||
@@ -37,7 +38,7 @@ const BoardHeader = () => {
|
||||
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
|
||||
{!useIsMobile() && (
|
||||
<div className={styles.bannerCnt}>
|
||||
<ImageBanner key={isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : address} />
|
||||
|
||||
@@ -28,6 +28,7 @@ import Tooltip from '../tooltip';
|
||||
import { PostProps } from '../../views/post/post';
|
||||
import { create } from 'zustand';
|
||||
import _ from 'lodash';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
|
||||
interface ShowOmittedRepliesState {
|
||||
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 { link, spoiler, cid } = post || {};
|
||||
|
||||
// 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 commentMediaInfo = useCommentMediaInfo(post);
|
||||
const { url } = commentMediaInfo || {};
|
||||
@@ -235,7 +236,6 @@ const PostMediaContent = ({ post, link, spoiler, t }: { post: any; link: string;
|
||||
}
|
||||
|
||||
const embedUrl = url && new URL(url);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
|
||||
const mediaDimensions = getMediaDimensions(commentMediaInfo);
|
||||
@@ -451,6 +451,9 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
replyCount: 0,
|
||||
};
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
<div className={styles.postDesktop}>
|
||||
{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>
|
||||
)}
|
||||
<div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
||||
{link && !isHidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} />}
|
||||
<div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid} className={shouldShowSnow() && hasThumbnail ? styles.xmasHatWrapper : ''}>
|
||||
{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} />
|
||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||
{!isHidden && <PostMessage post={post} />}
|
||||
|
||||
@@ -23,6 +23,7 @@ import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
import { PostProps } from '../../views/post/post';
|
||||
import _ from 'lodash';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
|
||||
const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -400,7 +401,13 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
)}
|
||||
<div className={showReplies ? styles.thread : styles.quotePreview}>
|
||||
<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} />
|
||||
<PostMessageMobile post={post} />
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import _ from 'lodash';
|
||||
import useInterfaceSettingsStore from '../../../stores/use-interface-settings-store';
|
||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-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 isElectron = window.isElectron === true;
|
||||
@@ -69,19 +70,33 @@ const CheckForUpdates = () => {
|
||||
|
||||
const Style = () => {
|
||||
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>) => {
|
||||
setTheme(e.target.value);
|
||||
const newTheme = e.target.value;
|
||||
|
||||
if (newTheme === 'special') {
|
||||
setIsEnabled(true);
|
||||
setTheme('tomorrow');
|
||||
} else {
|
||||
setIsEnabled(false);
|
||||
setTheme(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
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-b'>Yotsuba B</option>
|
||||
<option value='futaba'>Futaba</option>
|
||||
<option value='burichan'>Burichan</option>
|
||||
<option value='tomorrow'>Tomorrow</option>
|
||||
<option value='photon'>Photon</option>
|
||||
{isChristmas && <option value='special'>Special</option>}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import useDefaultSubplebbits from './use-default-subplebbits';
|
||||
import useInitialTheme from './use-initial-theme';
|
||||
import { nsfwTags } from '../views/home/home';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||
|
||||
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 pendingPost = useAccountComment({ commentIndex: pendingPostCommentIndex });
|
||||
const pendingPostSubplebbitAddress = pendingPost?.subplebbitAddress;
|
||||
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
|
||||
|
||||
const setThemeStore = useThemeStore((state) => state.setTheme);
|
||||
const getTheme = useThemeStore((state) => state.getTheme);
|
||||
@@ -32,11 +34,40 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
const initialTheme = useInitialTheme(pendingPostSubplebbitAddress);
|
||||
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 { isEnabled } = useSpecialThemeStore.getState();
|
||||
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
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;
|
||||
if (isInAllView || isInSubscriptionsView) {
|
||||
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;
|
||||
@@ -37,4 +37,14 @@
|
||||
.footer {
|
||||
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;
|
||||
}
|
||||
+45
-41
@@ -19,6 +19,7 @@ import SubplebbitDescription from '../../components/subplebbit-description';
|
||||
import SubplebbitRules from '../../components/subplebbit-rules';
|
||||
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
@@ -262,48 +263,51 @@ const Board = () => {
|
||||
}, [title, shortAddress, subplebbitAddress]);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{location.pathname.endsWith('/settings') && <SettingsModal />}
|
||||
{activeCid && threadCid && postSubplebbitAddress && (
|
||||
<ReplyModal
|
||||
closeModal={closeModal}
|
||||
parentCid={activeCid}
|
||||
postCid={threadCid}
|
||||
scrollY={scrollY}
|
||||
showReplyModal={showReplyModal}
|
||||
subplebbitAddress={postSubplebbitAddress}
|
||||
/>
|
||||
)}
|
||||
{((description && description.length > 0) || isInAllView) && (
|
||||
<SubplebbitDescription
|
||||
avatarUrl={suggested?.avatarUrl}
|
||||
subplebbitAddress={subplebbitAddress}
|
||||
createdAt={createdAt}
|
||||
description={description}
|
||||
replyCount={isInAllView ? 0 : rules?.length > 0 ? 1 : 0}
|
||||
shortAddress={shortAddress}
|
||||
title={title}
|
||||
/>
|
||||
)}
|
||||
{rules && !description && rules.length > 0 && <SubplebbitRules subplebbitAddress={subplebbitAddress} createdAt={createdAt} rules={rules} />}
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={combinedFeed.length}
|
||||
data={combinedFeed}
|
||||
itemContent={(index, post) => {
|
||||
const { deleted, locked, removed } = post || {};
|
||||
const isThreadClosed = deleted || locked || removed;
|
||||
<>
|
||||
{shouldShowSnow() && <hr />}
|
||||
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
|
||||
{location.pathname.endsWith('/settings') && <SettingsModal />}
|
||||
{activeCid && threadCid && postSubplebbitAddress && (
|
||||
<ReplyModal
|
||||
closeModal={closeModal}
|
||||
parentCid={activeCid}
|
||||
postCid={threadCid}
|
||||
scrollY={scrollY}
|
||||
showReplyModal={showReplyModal}
|
||||
subplebbitAddress={postSubplebbitAddress}
|
||||
/>
|
||||
)}
|
||||
{((description && description.length > 0) || isInAllView) && (
|
||||
<SubplebbitDescription
|
||||
avatarUrl={suggested?.avatarUrl}
|
||||
subplebbitAddress={subplebbitAddress}
|
||||
createdAt={createdAt}
|
||||
description={description}
|
||||
replyCount={isInAllView ? 0 : rules?.length > 0 ? 1 : 0}
|
||||
shortAddress={shortAddress}
|
||||
title={title}
|
||||
/>
|
||||
)}
|
||||
{rules && !description && rules.length > 0 && <SubplebbitRules subplebbitAddress={subplebbitAddress} createdAt={createdAt} rules={rules} />}
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={combinedFeed.length}
|
||||
data={combinedFeed}
|
||||
itemContent={(index, post) => {
|
||||
const { deleted, locked, removed } = post || {};
|
||||
const isThreadClosed = deleted || locked || removed;
|
||||
|
||||
return <Post index={index} post={post} openReplyModal={isThreadClosed ? () => alert(t('thread_closed_alert')) : openReplyModal} />;
|
||||
}}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
</div>
|
||||
return <Post index={index} post={post} openReplyModal={isThreadClosed ? () => alert(t('thread_closed_alert')) : openReplyModal} />;
|
||||
}}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
endReached={loadMore}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,30 @@
|
||||
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 */
|
||||
.postDesktop::after {
|
||||
content: "";
|
||||
|
||||
@@ -16,6 +16,7 @@ import styles from './post.module.css';
|
||||
export interface PostProps {
|
||||
index?: number;
|
||||
isHidden?: boolean;
|
||||
hasThumbnail?: boolean;
|
||||
post?: any;
|
||||
postReplyCount?: number;
|
||||
reply?: any;
|
||||
|
||||
Reference in New Issue
Block a user