Skip to content

Commit 3289a17

Browse files
Customizable Post previews (#338)
* init * init * Add settings to post preview editor * Improve settings * Fix active drop zone * Rewrite nesting, routing & previews * Fix padding & add background on/off toggle to themes * Update minimal template * Fix alignment * Address feedback * Fix overflow in settings * Fix styling issues * Fix border radius * fix: post preview type error --------- Co-authored-by: NickJ202 <nickjuliano20@gmail.com>
1 parent b019b47 commit 3289a17

24 files changed

Lines changed: 4083 additions & 111 deletions

File tree

package-lock.json

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"dependencies": {
3333
"@ar.io/sdk": "^3.16.0-alpha.1",
3434
"@ardrive/turbo-sdk": "^1.30.0",
35+
"@dnd-kit/core": "^6.3.1",
36+
"@dnd-kit/sortable": "^10.0.0",
37+
"@dnd-kit/utilities": "^3.2.2",
3538
"@hello-pangea/dnd": "^17.0.0",
3639
"@lexical/link": "^0.34.0",
3740
"@lexical/list": "^0.34.0",

src/apps/editor/components/molecules/Layout/Layout.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,18 @@ export default function Layout() {
169169
// Update local state when portal data changes (only if user hasn't made a selection)
170170
React.useEffect(() => {
171171
if (portalProvider.current?.layout && !originalLayoutSet.current) {
172-
setOriginalLayout(portalProvider.current.layout);
172+
setOriginalLayout({
173+
...portalProvider.current.layout,
174+
postPreviews: portalProvider.current.layout?.postPreviews ?? {},
175+
});
173176
originalLayoutSet.current = true;
174177
}
175178
if (hasUserSelected.current) return;
176179
if (portalProvider.current?.layout) {
177-
setLayout(portalProvider.current.layout);
180+
setLayout({
181+
...portalProvider.current.layout,
182+
postPreviews: portalProvider.current.layout?.postPreviews ?? {},
183+
});
178184
}
179185
if (portalProvider.current?.pages) {
180186
setPages(portalProvider.current.pages);
@@ -204,6 +210,19 @@ export default function Layout() {
204210

205211
function handleLayoutOptionChange(optionName: string) {
206212
hasUserSelected.current = true;
213+
const existingPostPreviews = portalProvider.current?.layout?.postPreviews ?? layout?.postPreviews ?? {};
214+
const layoutValue = optionName.toLowerCase();
215+
const updateFeedLayouts = (node: any): any => {
216+
if (!node) return node;
217+
if (Array.isArray(node)) return node.map((item) => updateFeedLayouts(item));
218+
if (node.type === 'feed') {
219+
return { ...node, layout: layoutValue };
220+
}
221+
if (node.content && Array.isArray(node.content)) {
222+
return { ...node, content: node.content.map((item: any) => updateFeedLayouts(item)) };
223+
}
224+
return node;
225+
};
207226
if (themes && Array.isArray(themes)) {
208227
const updatedThemes = themes.map((theme: any) => {
209228
if (!theme.active) return theme;
@@ -217,13 +236,15 @@ export default function Layout() {
217236
}
218237

219238
if (optionName === 'blog') {
220-
setLayout(LAYOUT.BLOG);
239+
setLayout({ ...LAYOUT.BLOG, postPreviews: existingPostPreviews });
240+
if (pages) setPages(updateFeedLayouts(pages));
221241
} else if (optionName === 'journal') {
222-
setLayout(LAYOUT.JOURNAL);
242+
setLayout({ ...LAYOUT.JOURNAL, postPreviews: existingPostPreviews });
243+
if (pages) setPages(updateFeedLayouts(pages));
223244
} else if (optionName === 'documentation') {
224-
setLayout(LAYOUT.DOCUMENTATION);
245+
setLayout({ ...LAYOUT.DOCUMENTATION, postPreviews: existingPostPreviews });
246+
if (pages) setPages(updateFeedLayouts(pages));
225247
} else {
226-
const layoutValue = optionName.toLowerCase();
227248
const updatedPages = {
228249
...pages,
229250
Feed: {
@@ -255,9 +276,13 @@ export default function Layout() {
255276
setLoading(true);
256277

257278
const updateData: any = {};
279+
const layoutWithPreviews = {
280+
...(layout || {}),
281+
postPreviews: layout?.postPreviews ?? portalProvider.current?.layout?.postPreviews ?? {},
282+
};
258283

259-
if (JSON.stringify(originalLayout) !== JSON.stringify(layout)) {
260-
updateData.Layout = permawebProvider.libs.mapToProcessCase(layout);
284+
if (JSON.stringify(originalLayout) !== JSON.stringify(layoutWithPreviews)) {
285+
updateData.Layout = permawebProvider.libs.mapToProcessCase(layoutWithPreviews);
261286
}
262287
if (JSON.stringify(originalPages) !== JSON.stringify(pages)) {
263288
updateData.Pages = permawebProvider.libs.mapToProcessCase(pages);

src/apps/editor/components/molecules/PageSection/FeedBlock/FeedBlock.tsx

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import React from 'react';
22

33
import { usePortalProvider } from 'editor/providers/PortalProvider';
44

5-
import { Button } from 'components/atoms/Button';
6-
import { ICONS } from 'helpers/config';
75
import { getTxEndpoint } from 'helpers/endpoints';
86
import { PageBlockType, PortalAssetType } from 'helpers/types';
9-
import { useLanguageProvider } from 'providers/LanguageProvider';
107

118
import * as S from './styles';
129

@@ -16,14 +13,8 @@ const FALLBACK_DATA = {
1613
description: 'Description',
1714
};
1815

19-
export default function FeedBlock(props: {
20-
index: number;
21-
block: PageBlockType;
22-
onChangeBlock: (block: PageBlockType, index: number) => void;
23-
}) {
16+
export default function FeedBlock(props: { index: number; block: PageBlockType }) {
2417
const portalProvider = usePortalProvider();
25-
const languageProvider = useLanguageProvider();
26-
const language = languageProvider.object[languageProvider.current];
2718

2819
const [posts, setPosts] = React.useState<PortalAssetType[]>([]);
2920

@@ -35,42 +26,9 @@ export default function FeedBlock(props: {
3526

3627
const displayPosts = posts.length > 0 ? posts.slice(0, 2) : [null, null];
3728

38-
// Ensure we always display both categories from the two posts
3929
const categoryNames = displayPosts.map((post) => post?.metadata?.categories?.[0]?.name || FALLBACK_DATA.category);
4030

41-
const options = [
42-
{ name: language.journal, value: 'journal' },
43-
{ name: language.blog, value: 'blog' },
44-
{ name: language.minimal, value: 'minimal' },
45-
];
46-
47-
const [currentOptionIndex, setCurrentOptionIndex] = React.useState(0);
48-
49-
React.useEffect(() => {
50-
if (props.block?.layout) {
51-
const index = options.findIndex((opt) => opt.value === props.block.layout);
52-
if (index !== -1 && index !== currentOptionIndex) {
53-
setCurrentOptionIndex(index);
54-
}
55-
}
56-
}, [props.block?.layout]);
57-
58-
const handleRotate = () => {
59-
const nextIndex = (currentOptionIndex + 1) % options.length;
60-
setCurrentOptionIndex(nextIndex);
61-
62-
props.onChangeBlock(
63-
{
64-
...props.block,
65-
layout: options[nextIndex].value,
66-
},
67-
props.index
68-
);
69-
};
70-
71-
const currentOption = options[currentOptionIndex];
72-
const currentLayout = props.block?.layout ?? options[0].value;
73-
const nextOption = options[(currentOptionIndex + 1) % options.length];
31+
const currentLayout = props.block?.layout ?? 'blog';
7432

7533
return (
7634
<S.Wrapper>
@@ -106,15 +64,6 @@ export default function FeedBlock(props: {
10664
);
10765
})}
10866
</S.ContentWrapper>
109-
<S.ActionsWrapper>
110-
<Button
111-
type={'alt3'}
112-
label={`Layout: ${currentOption.name}`}
113-
handlePress={handleRotate}
114-
icon={ICONS.rotate}
115-
tooltip={`${language.switchTo} ${nextOption.name}`}
116-
/>
117-
</S.ActionsWrapper>
11867
</S.Wrapper>
11968
);
12069
}

src/apps/editor/components/molecules/PageSection/FeedBlock/styles.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,3 @@ export const PostImage = styled.div<{ hasImage: boolean }>`
102102
padding: 30px;
103103
}
104104
`;
105-
106-
export const ActionsWrapper = styled.div`
107-
width: 100%;
108-
display: flex;
109-
align-items: center;
110-
justify-content: center;
111-
gap: 15px;
112-
padding: 15px 0 0 0;
113-
margin: 15px 0 0 0;
114-
border-top: 1px solid ${(props) => props.theme.colors.border.primary};
115-
`;

src/apps/editor/components/molecules/PageSection/PageSection.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ import { useDispatch, useSelector } from 'react-redux';
33
import { ReactSVG } from 'react-svg';
44
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
55

6+
import { usePortalProvider } from 'editor/providers/PortalProvider';
67
import { EditorStoreRootState } from 'editor/store';
78
import { currentPageUpdate } from 'editor/store/page';
89

910
import { Button } from 'components/atoms/Button';
1011
import { IconButton } from 'components/atoms/IconButton';
1112
import { Panel } from 'components/atoms/Panel';
12-
import { ARTICLE_BLOCKS, ICONS, PAGE_BLOCKS } from 'helpers/config';
13+
import { ARTICLE_BLOCKS, ICONS, PAGE_BLOCKS, POST_PREVIEWS } from 'helpers/config';
1314
import { ArticleBlockEnum, PageBlockEnum, PageBlockType, PageSectionEnum, PageSectionType } from 'helpers/types';
1415
import { capitalize } from 'helpers/utils';
1516
import { useLanguageProvider } from 'providers/LanguageProvider';
17+
import { CloseHandler } from 'wrappers/CloseHandler';
1618

1719
import { ArticleBlock } from '../ArticleBlock';
1820
import { ArticleBlocks } from '../ArticleBlocks';
@@ -48,9 +50,22 @@ export default function PageSection(props: {
4850
const { resizingBlockId: globalResizingBlockId, setResizingBlockId: setGlobalResizingBlockId } =
4951
React.useContext(ResizeContext);
5052

53+
const portalProvider = usePortalProvider();
5154
const languageProvider = useLanguageProvider();
5255
const language = languageProvider.object[languageProvider.current];
5356

57+
const [feedLayoutDropdown, setFeedLayoutDropdown] = React.useState<string | null>(null);
58+
59+
const feedLayoutOptions = React.useMemo(() => {
60+
const portalPreviews = portalProvider.current?.layout?.postPreviews || portalProvider.current?.postPreviews || {};
61+
const allTemplates = { ...POST_PREVIEWS, ...portalPreviews };
62+
63+
return Object.entries(allTemplates).map(([id, template]: [string, any]) => ({
64+
id: id,
65+
label: template.name || id,
66+
}));
67+
}, [portalProvider.current?.postPreviews]);
68+
5469
const handleCurrentPageUpdate = (updatedField: { field: string; value: any }) => {
5570
dispatch(currentPageUpdate(updatedField));
5671
};
@@ -426,7 +441,7 @@ export default function PageSection(props: {
426441
}
427442
switch (block.type) {
428443
case 'feed':
429-
return <FeedBlock index={index} key={block.id} block={block} onChangeBlock={handlePageBlockChange} />;
444+
return <FeedBlock index={index} key={block.id} block={block} />;
430445
case 'post':
431446
return <PostBlock index={index} key={block.id} block={block} onChangeBlock={handlePageBlockChange} />;
432447
case 'postSpotlight':
@@ -570,6 +585,44 @@ export default function PageSection(props: {
570585
noFocus
571586
/>
572587
)}
588+
{block.type === 'feed' && (
589+
<CloseHandler
590+
active={feedLayoutDropdown === block.id}
591+
disabled={feedLayoutDropdown !== block.id}
592+
callback={() => setFeedLayoutDropdown(null)}
593+
>
594+
<S.FeedLayoutWrapper>
595+
<IconButton
596+
type={'alt1'}
597+
active={feedLayoutDropdown === block.id}
598+
src={ICONS.layout}
599+
handlePress={() =>
600+
setFeedLayoutDropdown(feedLayoutDropdown === block.id ? null : block.id)
601+
}
602+
dimensions={{ wrapper: 23.5, icon: 13.5 }}
603+
tooltip={language?.postLayout || 'Post Layout'}
604+
tooltipPosition={'bottom-right'}
605+
noFocus
606+
/>
607+
{feedLayoutDropdown === block.id && (
608+
<S.FeedLayoutDropdown>
609+
{feedLayoutOptions.map((option) => (
610+
<S.FeedLayoutOption
611+
key={option.id}
612+
$active={block.layout === option.id}
613+
onClick={() => {
614+
handlePageBlockChange({ ...block, layout: option.id }, index);
615+
setFeedLayoutDropdown(null);
616+
}}
617+
>
618+
{option.label}
619+
</S.FeedLayoutOption>
620+
))}
621+
</S.FeedLayoutDropdown>
622+
)}
623+
</S.FeedLayoutWrapper>
624+
</CloseHandler>
625+
)}
573626
<IconButton
574627
type={'alt1'}
575628
active={false}

src/apps/editor/components/molecules/PageSection/styles.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,39 @@ export const SubElementHeaderActions = styled.div`
189189
gap: 5px;
190190
`;
191191

192+
export const FeedLayoutWrapper = styled.div`
193+
position: relative;
194+
`;
195+
196+
export const FeedLayoutDropdown = styled.div`
197+
position: absolute;
198+
top: 100%;
199+
right: 0;
200+
margin-top: 5px;
201+
min-width: 120px;
202+
background: ${(props) => props.theme.colors.container.primary.background};
203+
border: 1px solid ${(props) => props.theme.colors.border.primary};
204+
border-radius: ${STYLING.dimensions.radius.alt2};
205+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
206+
z-index: 100;
207+
overflow: hidden;
208+
`;
209+
210+
export const FeedLayoutOption = styled.div<{ $active: boolean }>`
211+
padding: 8px 12px;
212+
font-size: ${(props) => props.theme.typography.size.xxSmall};
213+
font-weight: ${(props) => props.theme.typography.weight.medium};
214+
color: ${(props) => (props.$active ? props.theme.colors.font.primary : props.theme.colors.font.alt1)};
215+
background: ${(props) => (props.$active ? props.theme.colors.container.primary.active : 'transparent')};
216+
cursor: pointer;
217+
transition: all 100ms;
218+
219+
&:hover {
220+
background: ${(props) => props.theme.colors.container.primary.active};
221+
color: ${(props) => props.theme.colors.font.primary};
222+
}
223+
`;
224+
192225
export const SubElementBody = styled.div`
193226
width: 100%;
194227
`;

0 commit comments

Comments
 (0)