Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions build/577.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/social-web/followers.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build/social-web/following.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build/social-web/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-commands', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-primitives', 'wp-private-apis', 'wp-url', 'wp-warning'), 'version' => 'dcd1fc89c94fc175e1b4');
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-commands', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-primitives', 'wp-private-apis', 'wp-router', 'wp-url', 'wp-warning'), 'version' => 'd1f73cf72171bb76157e');
118 changes: 2 additions & 116 deletions build/social-web/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/social-web/interactions.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

190 changes: 78 additions & 112 deletions src/social-web/components/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,72 +7,28 @@
* - Inspector (380px fixed, optional) - Detail panel
*/

import { useState, useEffect } from '@wordpress/element';
import { useEffect, useCallback, Suspense } from '@wordpress/element';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { addQueryArgs } from '@wordpress/url';
import { CommandMenu } from '@wordpress/commands';
import { Spinner } from '@wordpress/components';
import { unlock } from '../../lock-unlock';
import Sidebar from '../sidebar';
import Panel from '../panel';
import './style.scss';

// Import stage components.
// Import dashboard separately since it's not in route areas
import DashboardStage from '../../routes/dashboard/stage';
import FollowersStage from '../../routes/followers/stage';
import FollowingStage from '../../routes/following/stage';
import InteractionsStage from '../../routes/interactions/stage';

// Import inspector components.
import FollowerInspector from '../../routes/followers/inspector';
import FollowingInspector from '../../routes/following/inspector';
import InteractionInspector from '../../routes/interactions/inspector';

/**
* Parse the URL hash to extract section and item ID
* Format: #/section or #/section/itemId
*/
function parseHash(): { section: string; itemId: string | null } {
const hash = window.location.hash.slice( 1 ); // Remove #
if ( ! hash || hash === '/' ) {
return { section: 'dashboard', itemId: null };
}

const parts = hash.split( '/' ).filter( Boolean );
const section = parts[ 0 ] || 'dashboard';
const itemId = parts[ 1 ] || null;

return { section, itemId };
}

/**
* Update the URL hash without triggering a page reload
*/
function updateHash( section: string, itemId?: string | null ) {
const hash = itemId ? `#/${ section }/${ itemId }` : `#/${ section }`;
window.history.pushState( null, '', hash );
}
const { useLocation, useHistory } = unlock( routerPrivateApis );

export function Layout() {
const [ activeSection, setActiveSection ] = useState( 'dashboard' );
const [ selectedItemId, setSelectedItemId ] = useState< string | null >( null );

// Initialize from URL hash on mount
useEffect( () => {
const { section, itemId } = parseHash();
setActiveSection( section );
setSelectedItemId( itemId );
}, [] );
// Following Gutenberg's pattern: destructure what we need from location
const { query, name: activeSection = 'dashboard', areas = {} } = useLocation();
const history = useHistory();

// Listen for hash changes (back/forward navigation)
useEffect( () => {
const handleHashChange = () => {
const { section, itemId } = parseHash();
setActiveSection( section );
setSelectedItemId( itemId );
};

window.addEventListener( 'hashchange', handleHashChange );
return () => {
window.removeEventListener( 'hashchange', handleHashChange );
};
}, [] );
// Get itemId from query params
const selectedItemId = ( query?.itemId as string ) || null;

// Add fullscreen mode class to body
useEffect( () => {
Expand All @@ -82,62 +38,72 @@ export function Layout() {
};
}, [] );

const handleSelectItem = ( id: string ) => {
setSelectedItemId( id );
updateHash( activeSection, id );
};

const handleCloseInspector = () => {
setSelectedItemId( null );
updateHash( activeSection );
};

const handleNavigate = ( section: string ) => {
setActiveSection( section );
setSelectedItemId( null );
updateHash( section );
};

// Render main content (stage)
const renderStage = () => {
const props = { onSelectItem: handleSelectItem };

switch ( activeSection ) {
case 'dashboard':
return <DashboardStage />;
case 'followers':
return <FollowersStage { ...props } />;
case 'following':
return <FollowingStage { ...props } />;
case 'interactions':
return <InteractionsStage { ...props } />;
default:
return <DashboardStage />;
}
};

// Render detail panel (inspector)
const renderInspector = () => {
if ( ! selectedItemId ) return null;

const props = { id: selectedItemId, onClose: handleCloseInspector };

switch ( activeSection ) {
case 'followers':
return <FollowerInspector { ...props } />;
case 'following':
return <FollowingInspector { ...props } />;
case 'interactions':
return <InteractionInspector { ...props } />;
default:
return null;
}
};

const showInspector = !! selectedItemId;
const handleSelectItem = useCallback(
( id: string ) => {
// Navigate with itemId in query params
const queryString = addQueryArgs( '', {
...query,
itemId: id,
} );
// Map section names to paths (dashboard is '/', others are '/{section}')
const path = activeSection === 'dashboard' ? '/' : `/${ activeSection }`;
history.navigate( `${ path }${ queryString }` );
},
[ query, activeSection, history ]
);

const handleCloseInspector = useCallback( () => {
// Remove itemId from query
const { itemId, ...restQuery } = query || {};
const queryString = Object.keys( restQuery ).length > 0 ? addQueryArgs( '', restQuery ) : '';
// Map section names to paths (dashboard is '/', others are '/{section}')
const path = activeSection === 'dashboard' ? '/' : `/${ activeSection }`;
history.navigate( `${ path }${ queryString }` );
}, [ query, activeSection, history ] );

const handleNavigate = useCallback(
( section: string ) => {
// Navigate to new section, preserve non-itemId query params
const { itemId, ...restQuery } = query || {};
const queryString = Object.keys( restQuery ).length > 0 ? addQueryArgs( '', restQuery ) : '';

// Map section names to paths (dashboard is '/', others are '/{section}')
const path = section === 'dashboard' ? '/' : `/${ section }`;
history.navigate( `${ path }${ queryString }` );
},
[ query, history ]
);

// Render stage component from route areas or use DashboardStage for dashboard
const StageComponent = areas?.stage;
let stageElement;
if ( StageComponent ) {
stageElement = (
<Suspense fallback={ <Spinner /> }>
<StageComponent onSelectItem={ handleSelectItem } />
</Suspense>
);
} else if ( activeSection === 'dashboard' ) {
stageElement = <DashboardStage />;
} else {
stageElement = null;
}

// Render inspector component from route areas
const InspectorComponent = areas?.inspector;
let inspectorElement = null;
if ( selectedItemId && InspectorComponent ) {
inspectorElement = (
<Suspense fallback={ <Spinner /> }>
<InspectorComponent id={ selectedItemId } onClose={ handleCloseInspector } />
</Suspense>
);
}

const showInspector = !! inspectorElement;

return (
<div className="app-layout">
<div className="app-layout" data-section={ activeSection }>
<CommandMenu />
<div className="app-content">
{ /* Sidebar - 240px fixed width (no Panel wrapper, stays dark) */ }
Expand All @@ -147,13 +113,13 @@ export function Layout() {

{ /* Stage - main content area */ }
<div className="stage-region">
<Panel>{ renderStage() }</Panel>
<Panel>{ stageElement }</Panel>
</div>

{ /* Inspector - optional 380px side panel */ }
{ showInspector && (
<div className="inspector-region">
<Panel>{ renderInspector() }</Panel>
<Panel>{ inspectorElement }</Panel>
</div>
) }
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/social-web/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
MenuGroup,
MenuItem,
NavigableMenu,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- HStack is widely used in core and stable in practice
__experimentalHStack as HStack,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- Heading is widely used in core and stable in practice
__experimentalHeading as Heading,
} from '@wordpress/components';
import { home, people, addCard, comment, chevronRight, chevronLeft } from '@wordpress/icons';
Expand Down
7 changes: 6 additions & 1 deletion src/social-web/components/site-hub/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { Button, __experimentalHStack as HStack, VisuallyHidden } from '@wordpress/components';
import {
Button,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- HStack is widely used in core and stable in practice
__experimentalHStack as HStack,
VisuallyHidden,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
Expand Down
50 changes: 35 additions & 15 deletions src/social-web/hooks/use-social-web-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ function useSocialWebDataFull(): SocialWebData & SocialWebActions {
fetchFollowers();
fetchFollowing();
fetchInteractions();
// Dependencies are dispatch functions from WordPress data store
// which are stable references and safe to omit per WordPress patterns
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );

return {
Expand All @@ -82,6 +85,10 @@ function useSocialWebDataFull(): SocialWebData & SocialWebActions {

/**
* Hook to access Social Web data with optional resource filtering
*
* @param resource - Optional resource type to filter ('followers' | 'following' | 'interactions')
* @param id - Optional ID to fetch a specific item
* @return Object containing items and loading state
*/
export function useSocialWebData(
resource?: 'followers' | 'following' | 'interactions',
Expand All @@ -92,6 +99,25 @@ export function useSocialWebData(
} {
const allData = useSocialWebDataFull();

// Always call useSelect to comply with Rules of Hooks
const item = useSelect(
( select ) => {
if ( ! id || ! resource ) {
return null;
}
const store = select( STORE_NAME ) as any;
if ( resource === 'followers' ) {
return store.getFollowerById( id ) as Follower | undefined;
} else if ( resource === 'following' ) {
return store.getFollowingById( id ) as Following | undefined;
} else if ( resource === 'interactions' ) {
return store.getInteractionById( id ) as Interaction | undefined;
}
return null;
},
[ resource, id ]
);

if ( ! resource ) {
// Return all data if no resource specified
return {
Expand All @@ -102,21 +128,6 @@ export function useSocialWebData(

if ( id ) {
// Return single item
const item = useSelect(
( select ) => {
const store = select( STORE_NAME ) as any;
if ( resource === 'followers' ) {
return store.getFollowerById( id ) as Follower | undefined;
} else if ( resource === 'following' ) {
return store.getFollowingById( id ) as Following | undefined;
} else if ( resource === 'interactions' ) {
return store.getInteractionById( id ) as Interaction | undefined;
}
return null;
},
[ resource, id ]
);

return {
items: item,
isLoading: allData.isLoading[ resource ],
Expand All @@ -132,6 +143,9 @@ export function useSocialWebData(

/**
* Hook to get a specific follower by ID
*
* @param id - The follower ID
* @return The follower object or undefined if not found
*/
export function useFollower( id: string ): Follower | undefined {
return useSelect(
Expand All @@ -145,6 +159,9 @@ export function useFollower( id: string ): Follower | undefined {

/**
* Hook to get a specific following by ID
*
* @param id - The following ID
* @return The following object or undefined if not found
*/
export function useFollowing( id: string ): Following | undefined {
return useSelect(
Expand All @@ -158,6 +175,9 @@ export function useFollowing( id: string ): Following | undefined {

/**
* Hook to get a specific interaction by ID
*
* @param id - The interaction ID
* @return The interaction object or undefined if not found
*/
export function useInteraction( id: string ): Interaction | undefined {
return useSelect(
Expand Down
Loading
Loading