This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: make semble stats clickable to switch tabs

+114 -45
+48
src/webapp/features/semble/components/sembleStats/SembleStatItem.tsx
··· 1 + 'use client'; 2 + 3 + import { UnstyledButton } from '@mantine/core'; 4 + import { ReactNode } from 'react'; 5 + 6 + export const SEMBLE_TAB_CHANGE_EVENT = 'semble:tab-change'; 7 + 8 + export type SembleStatTab = 9 + | 'notes' 10 + | 'collections' 11 + | 'addedBy' 12 + | 'connections'; 13 + 14 + interface Props { 15 + tab: SembleStatTab; 16 + children: ReactNode; 17 + } 18 + 19 + export default function SembleStatItem(props: Props) { 20 + const handleClick = () => { 21 + const params = new URLSearchParams(window.location.search); 22 + const queryParts: string[] = []; 23 + let foundTab = false; 24 + params.forEach((value, key) => { 25 + if (key === 'sembleTab') { 26 + queryParts.push(`sembleTab=${props.tab}`); 27 + foundTab = true; 28 + } else { 29 + queryParts.push(`${key}=${value}`); 30 + } 31 + }); 32 + if (!foundTab) { 33 + queryParts.push(`sembleTab=${props.tab}`); 34 + } 35 + window.history.replaceState(null, '', `?${queryParts.join('&')}`); 36 + window.dispatchEvent( 37 + new CustomEvent<SembleStatTab>(SEMBLE_TAB_CHANGE_EVENT, { 38 + detail: props.tab, 39 + }), 40 + ); 41 + }; 42 + 43 + return ( 44 + <UnstyledButton onClick={handleClick} style={{ cursor: 'pointer' }}> 45 + {props.children} 46 + </UnstyledButton> 47 + ); 48 + }
+53 -44
src/webapp/features/semble/components/sembleStats/SembleStats.tsx
··· 4 4 import { BiCollection, BiLink } from 'react-icons/bi'; 5 5 import { MdOutlineStickyNote2 } from 'react-icons/md'; 6 6 import { LuLibrary } from 'react-icons/lu'; 7 + import SembleStatItem from './SembleStatItem'; 7 8 8 9 interface Props { 9 10 url: string; ··· 19 20 20 21 return ( 21 22 <Group> 22 - <Group gap={5}> 23 - <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 24 - <LuLibrary /> 25 - </ThemeIcon> 23 + <SembleStatItem tab="addedBy"> 24 + <Group gap={5}> 25 + <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 26 + <LuLibrary /> 27 + </ThemeIcon> 26 28 27 - <Text fw={500} fz={'sm'} c={'bright'}> 28 - {stats?.libraryCount} 29 - </Text> 30 - <Text fw={500} fz={'sm'} c={'dimmed'}> 31 - {stats?.libraryCount === 1 ? 'Save' : 'Saves'} 32 - </Text> 33 - </Group> 29 + <Text fw={500} fz={'sm'} c={'bright'}> 30 + {stats?.libraryCount} 31 + </Text> 32 + <Text fw={500} fz={'sm'} c={'dimmed'}> 33 + {stats?.libraryCount === 1 ? 'Save' : 'Saves'} 34 + </Text> 35 + </Group> 36 + </SembleStatItem> 34 37 35 - <Group gap={5}> 36 - <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 37 - <BiCollection /> 38 - </ThemeIcon> 39 - <Text fw={500} fz={'sm'} c={'bright'}> 40 - {stats?.collectionCount} 41 - </Text> 42 - <Text fw={500} fz={'sm'} c={'dimmed'}> 43 - {stats?.collectionCount === 1 ? 'Collection' : 'Collections'} 44 - </Text> 45 - </Group> 38 + <SembleStatItem tab="collections"> 39 + <Group gap={5}> 40 + <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 41 + <BiCollection /> 42 + </ThemeIcon> 43 + <Text fw={500} fz={'sm'} c={'bright'}> 44 + {stats?.collectionCount} 45 + </Text> 46 + <Text fw={500} fz={'sm'} c={'dimmed'}> 47 + {stats?.collectionCount === 1 ? 'Collection' : 'Collections'} 48 + </Text> 49 + </Group> 50 + </SembleStatItem> 46 51 47 - <Group gap={5}> 48 - <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 49 - <BiLink /> 50 - </ThemeIcon> 51 - <Text fw={500} fz={'sm'} c={'bright'}> 52 - {stats?.connections.all.total} 53 - </Text> 54 - <Text fw={500} fz={'sm'} c={'dimmed'}> 55 - {stats?.connections.all.total === 1 ? 'Connection' : 'Connections'} 56 - </Text> 57 - </Group> 52 + <SembleStatItem tab="connections"> 53 + <Group gap={5}> 54 + <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 55 + <BiLink /> 56 + </ThemeIcon> 57 + <Text fw={500} fz={'sm'} c={'bright'}> 58 + {stats?.connections.all.total} 59 + </Text> 60 + <Text fw={500} fz={'sm'} c={'dimmed'}> 61 + {stats?.connections.all.total === 1 ? 'Connection' : 'Connections'} 62 + </Text> 63 + </Group> 64 + </SembleStatItem> 58 65 59 - <Group gap={5}> 60 - <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 61 - <MdOutlineStickyNote2 /> 62 - </ThemeIcon> 63 - <Text fw={500} fz={'sm'} c={'bright'}> 64 - {stats?.noteCount} 65 - </Text> 66 - <Text fw={500} fz={'sm'} c={'dimmed'}> 67 - {stats?.noteCount === 1 ? 'Note' : 'Notes'} 68 - </Text> 69 - </Group> 66 + <SembleStatItem tab="notes"> 67 + <Group gap={5}> 68 + <ThemeIcon variant="transparent" color="cyan" size={'sm'}> 69 + <MdOutlineStickyNote2 /> 70 + </ThemeIcon> 71 + <Text fw={500} fz={'sm'} c={'bright'}> 72 + {stats?.noteCount} 73 + </Text> 74 + <Text fw={500} fz={'sm'} c={'dimmed'}> 75 + {stats?.noteCount === 1 ? 'Note' : 'Notes'} 76 + </Text> 77 + </Group> 78 + </SembleStatItem> 70 79 </Group> 71 80 ); 72 81 }
+13 -1
src/webapp/features/semble/components/sembleTabs/SembleTabs.tsx
··· 1 1 'use client'; 2 2 3 - import { useState, Suspense } from 'react'; 3 + import { useState, Suspense, useEffect } from 'react'; 4 4 import { useSearchParams } from 'next/navigation'; 5 + import { SEMBLE_TAB_CHANGE_EVENT } from '../sembleStats/SembleStatItem'; 5 6 import { 6 7 Box, 7 8 Container, ··· 70 71 }); 71 72 72 73 const stats = urlMetadata?.stats; 74 + 75 + useEffect(() => { 76 + const handler = (event: Event) => { 77 + const detail = (event as CustomEvent<TabValue>).detail; 78 + if (VALID_TABS.includes(detail)) { 79 + setActiveTab(detail); 80 + } 81 + }; 82 + window.addEventListener(SEMBLE_TAB_CHANGE_EVENT, handler); 83 + return () => window.removeEventListener(SEMBLE_TAB_CHANGE_EVENT, handler); 84 + }, []); 73 85 74 86 return ( 75 87 <Tabs