kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
just to save
rodzic
0240d6168b
commit
2bdf76a1bc
|
@ -141,6 +141,7 @@
|
|||
"reselect": "^5.0.0",
|
||||
"sass": "^1.79.5",
|
||||
"stringz": "^2.0.0",
|
||||
"swiper": "^11.2.4",
|
||||
"type-fest": "^4.0.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^6.0.2",
|
||||
|
|
|
@ -34,7 +34,7 @@ const messages = defineMessages({
|
|||
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
||||
});
|
||||
|
||||
const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||
export const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
||||
const history = useHistory();
|
||||
const [missing, setMissing] = useState<boolean>(false);
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ const ExplorerCards = () => {
|
|||
src={arrowIcon}
|
||||
theme='transparent'
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`transition-transform duration-300${
|
||||
className={`transition-transform duration-300 ${
|
||||
isOpen ? 'rotate-0' : 'rotate-180'
|
||||
}`}
|
||||
/>
|
|
@ -107,7 +107,7 @@ const ExplorerFilter = () => {
|
|||
const [showReplies, setShowReplies] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [include, setInclude] = useState(true);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const hasValue = inputValue.length > 0;
|
||||
|
||||
|
@ -147,7 +147,7 @@ const ExplorerFilter = () => {
|
|||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`group m-1 flex items-center gap-0.5 whitespace-normal break-words rounded-full border-2 bg-transparent px-3 text-lg font-medium shadow-sm hover:cursor-pointer hover:pr-1 ${borderColor} `}
|
||||
className={`group m-1 flex items-center gap-0.5 whitespace-normal break-words rounded-full border-2 bg-transparent px-3 text-base font-medium shadow-sm hover:cursor-pointer hover:pr-1 ${borderColor} `}
|
||||
>
|
||||
{name}
|
||||
<IconButton
|
||||
|
@ -163,7 +163,7 @@ const ExplorerFilter = () => {
|
|||
<Stack className='px-4' space={3}>
|
||||
|
||||
{/* Filters */}
|
||||
<HStack alignItems='start' space={1}>
|
||||
<HStack alignItems='start' justifyContent='between' space={1}>
|
||||
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
|
||||
<Text size='lg' weight='bold'>
|
||||
{intl.formatMessage(messages.filters)}
|
||||
|
@ -175,7 +175,7 @@ const ExplorerFilter = () => {
|
|||
<IconButton
|
||||
src={arrowIcon}
|
||||
theme='transparent'
|
||||
className={`transition-transform duration-300${ isOpen ? 'rotate-0' : 'rotate-180'}`}
|
||||
className={`transition-transform duration-300 ${ isOpen ? 'rotate-0' : 'rotate-180'}`}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
/>
|
||||
</HStack>
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
|
||||
import { useAccount } from 'soapbox/api/hooks/index.ts';
|
||||
import { InstanceFavicon } from 'soapbox/components/account.tsx';
|
||||
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import Text from 'soapbox/components/ui/text.tsx';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button.tsx';
|
||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||
import {
|
||||
// useDismissSuggestion,
|
||||
useSuggestions,
|
||||
} from 'soapbox/queries/suggestions.ts';
|
||||
|
||||
import 'swiper/css';
|
||||
|
||||
// Delete
|
||||
const lightenColor = (rgb: string, percent: number) => {
|
||||
const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||||
if (!match) return '#888888';
|
||||
|
||||
let r = parseInt(match[1]);
|
||||
let g = parseInt(match[2]);
|
||||
let b = parseInt(match[3]);
|
||||
|
||||
r = Math.min(255, r + (255 - r) * percent / 100);
|
||||
g = Math.min(255, g + (255 - g) * percent / 100);
|
||||
b = Math.min(255, b + (255 - b) * percent / 100);
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
// Delete
|
||||
const getFaviconColor = (src: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) return resolve('#888888');
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height).data;
|
||||
let r = 0, g = 0, b = 0, count = 0;
|
||||
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
r += imageData[i];
|
||||
g += imageData[i + 1];
|
||||
b += imageData[i + 2];
|
||||
count++;
|
||||
}
|
||||
|
||||
r = Math.floor(r / count);
|
||||
g = Math.floor(g / count);
|
||||
b = Math.floor(b / count);
|
||||
|
||||
resolve(`rgb(${r}, ${g}, ${b})`);
|
||||
};
|
||||
|
||||
img.onerror = () => resolve('#888888');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const PeopleToFollowCard = ({ id }: { id: string }) => {
|
||||
const account = useAccount(id).account;
|
||||
const { logo } = useSoapboxConfig();
|
||||
const [bgColor, setBgColor] = useState<string>('#888888');
|
||||
|
||||
useEffect(() => {
|
||||
if (account?.pleroma?.favicon) {
|
||||
getFaviconColor(account.pleroma.favicon).then((color) => {
|
||||
setBgColor(lightenColor(color, 0));
|
||||
}).catch(() => setBgColor('#888888'));
|
||||
}
|
||||
}, [account?.pleroma?.favicon]);
|
||||
|
||||
return (
|
||||
<Stack className='rounded-lg' >
|
||||
<Stack
|
||||
justifyContent='between' className='h-72 min-w-44 rounded-lg border border-primary-300 shadow-card-inset'
|
||||
style={{
|
||||
backgroundImage: `url(${account?.header ?? logo})`,
|
||||
backgroundSize: `${account?.header ? 'cover' : 'auto' }`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
height: 'full',
|
||||
}}
|
||||
space={3}
|
||||
>
|
||||
|
||||
{account && (<>
|
||||
|
||||
<HStack className='p-2'>
|
||||
<HStack
|
||||
alignItems='center' space={1} className='max-w-28 rounded-full border px-2 py-0.5 !text-white' style={{
|
||||
backgroundColor: bgColor,
|
||||
borderColor: bgColor,
|
||||
}}
|
||||
>
|
||||
<InstanceFavicon account={account} />
|
||||
<Text className='!text-white' size='xs' truncate>
|
||||
{account.domain}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Stack alignItems='center' justifyContent='center' className='pb-6' space={2}>
|
||||
<Avatar className='border border-white' src={account.avatar} size={60} />
|
||||
<Text className='w-32 text-center text-white' truncate>
|
||||
{account.display_name}
|
||||
</Text>
|
||||
<ActionButton account={account} />
|
||||
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const AccountsCarousel = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { data: suggestions, isFetching } = useSuggestions();
|
||||
// const dismissSuggestion = useDismissSuggestion();
|
||||
|
||||
// const handleDismiss = (account: AccountEntity) => {
|
||||
// dismissSuggestion.mutate(account.id);
|
||||
// };
|
||||
|
||||
if (!isFetching && !suggestions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack space={4}>
|
||||
<HStack className='px-4'>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='column.explorer.popular_accounts' defaultMessage={'Popular Accounts'} />
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack className='overflow-hidden px-4 '>
|
||||
<Swiper
|
||||
spaceBetween={10}
|
||||
slidesPerView={isMobile ? 2 : 3}
|
||||
grabCursor
|
||||
loop
|
||||
className='w-full'
|
||||
>
|
||||
{suggestions.map((suggestion) => (
|
||||
<SwiperSlide key={suggestion.account}>
|
||||
<PeopleToFollowCard id={suggestion.account} />
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</HStack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsCarousel;
|
|
@ -5,8 +5,9 @@ import Divider from 'soapbox/components/ui/divider.tsx';
|
|||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import SearchResults from 'soapbox/features/compose/components/search-results.tsx';
|
||||
import Search from 'soapbox/features/compose/components/search.tsx';
|
||||
import ExplorerCards from 'soapbox/features/search/components/explorerCards.tsx';
|
||||
import ExplorerCards from 'soapbox/features/search/components/explorer-cards.tsx';
|
||||
import ExplorerFilter from 'soapbox/features/search/components/explorerFilter.tsx';
|
||||
import AccountsCarousel from 'soapbox/features/search/components/people-to-follow-card.tsx';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.search', defaultMessage: 'Explorer' },
|
||||
|
@ -21,12 +22,16 @@ const SearchPage = () => {
|
|||
<Stack space={4}>
|
||||
<ExplorerCards />
|
||||
|
||||
<Divider text='Explorer' />
|
||||
<Divider text='Filters' />
|
||||
|
||||
<ExplorerFilter />
|
||||
|
||||
<Divider />
|
||||
|
||||
<AccountsCarousel />
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className='px-4'>
|
||||
<Search autoSubmit />
|
||||
</div>
|
||||
|
|
|
@ -438,6 +438,7 @@
|
|||
"column_forbidden.body": "You do not have permission to access this page.",
|
||||
"column_forbidden.title": "Forbidden",
|
||||
"column.explorer": "Explorer",
|
||||
"column.explorer.popular_accounts": "Popular Accounts",
|
||||
"column.explorer.welcome_card.title": "Welcome to Explorer",
|
||||
"column.explorer.welcome_card.text": "Explore the world of <span>decentralized social media</span>, dive into <span>Nostr</span> or cross the bridge to other networks, and connect with a global community. All in one place.",
|
||||
"column.explorer.nostr_card.title": "Nostr",
|
||||
|
|
|
@ -22,6 +22,10 @@ const config: Config = {
|
|||
boxShadow: ({ theme }) => ({
|
||||
'3xl': '0 25px 75px -15px rgba(0, 0, 0, 0.25)',
|
||||
'inset-ring': `inset 0 0 0 2px ${theme('colors.accent-blue')}`,
|
||||
'card': `rgba(0, 0, 0, 0.35}, 0.1) 0px 4px 16px,
|
||||
rgba(0, 0, 0, 0.35}, 0.1) 0px 8px 24px,
|
||||
rgba(0, 0, 0, 0.35}, 0.1) 0px 16px 56px`,
|
||||
'card-inset': 'rgba(0, 0, 0, 0.60) 0px -120px 36px -28px inset',
|
||||
}),
|
||||
fontSize: {
|
||||
base: '0.9375rem',
|
||||
|
|
|
@ -7989,6 +7989,11 @@ svgo@^3.0.2:
|
|||
csso "^5.0.5"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
swiper@^11.2.4:
|
||||
version "11.2.4"
|
||||
resolved "https://registry.yarnpkg.com/swiper/-/swiper-11.2.4.tgz#4d1b55e07f909957e0406bdbff81b850b9dc8aeb"
|
||||
integrity sha512-DTtglrsFfMYytid+oNy4QI3t2N2+XhhwSYbnyOhlwBmvY8Bkoj3ombK1/b80w8vDpQ+Lqlnbm+0737+i32MrcA==
|
||||
|
||||
symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
|
|
Ładowanie…
Reference in New Issue