kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
just to save
rodzic
0240d6168b
commit
2bdf76a1bc
|
@ -141,6 +141,7 @@
|
||||||
"reselect": "^5.0.0",
|
"reselect": "^5.0.0",
|
||||||
"sass": "^1.79.5",
|
"sass": "^1.79.5",
|
||||||
"stringz": "^2.0.0",
|
"stringz": "^2.0.0",
|
||||||
|
"swiper": "^11.2.4",
|
||||||
"type-fest": "^4.0.0",
|
"type-fest": "^4.0.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^6.0.2",
|
"vite": "^6.0.2",
|
||||||
|
|
|
@ -34,7 +34,7 @@ const messages = defineMessages({
|
||||||
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
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 history = useHistory();
|
||||||
const [missing, setMissing] = useState<boolean>(false);
|
const [missing, setMissing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ const ExplorerCards = () => {
|
||||||
src={arrowIcon}
|
src={arrowIcon}
|
||||||
theme='transparent'
|
theme='transparent'
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={`transition-transform duration-300${
|
className={`transition-transform duration-300 ${
|
||||||
isOpen ? 'rotate-0' : 'rotate-180'
|
isOpen ? 'rotate-0' : 'rotate-180'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
|
@ -107,7 +107,7 @@ const ExplorerFilter = () => {
|
||||||
const [showReplies, setShowReplies] = useState(false);
|
const [showReplies, setShowReplies] = useState(false);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [include, setInclude] = useState(true);
|
const [include, setInclude] = useState(true);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const hasValue = inputValue.length > 0;
|
const hasValue = inputValue.length > 0;
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ const ExplorerFilter = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={name}
|
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}
|
{name}
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -163,7 +163,7 @@ const ExplorerFilter = () => {
|
||||||
<Stack className='px-4' space={3}>
|
<Stack className='px-4' space={3}>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<HStack alignItems='start' space={1}>
|
<HStack alignItems='start' justifyContent='between' space={1}>
|
||||||
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
|
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
|
||||||
<Text size='lg' weight='bold'>
|
<Text size='lg' weight='bold'>
|
||||||
{intl.formatMessage(messages.filters)}
|
{intl.formatMessage(messages.filters)}
|
||||||
|
@ -175,7 +175,7 @@ const ExplorerFilter = () => {
|
||||||
<IconButton
|
<IconButton
|
||||||
src={arrowIcon}
|
src={arrowIcon}
|
||||||
theme='transparent'
|
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)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</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 Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import SearchResults from 'soapbox/features/compose/components/search-results.tsx';
|
import SearchResults from 'soapbox/features/compose/components/search-results.tsx';
|
||||||
import Search from 'soapbox/features/compose/components/search.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 ExplorerFilter from 'soapbox/features/search/components/explorerFilter.tsx';
|
||||||
|
import AccountsCarousel from 'soapbox/features/search/components/people-to-follow-card.tsx';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.search', defaultMessage: 'Explorer' },
|
heading: { id: 'column.search', defaultMessage: 'Explorer' },
|
||||||
|
@ -21,12 +22,16 @@ const SearchPage = () => {
|
||||||
<Stack space={4}>
|
<Stack space={4}>
|
||||||
<ExplorerCards />
|
<ExplorerCards />
|
||||||
|
|
||||||
<Divider text='Explorer' />
|
<Divider text='Filters' />
|
||||||
|
|
||||||
<ExplorerFilter />
|
<ExplorerFilter />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
<AccountsCarousel />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<div className='px-4'>
|
<div className='px-4'>
|
||||||
<Search autoSubmit />
|
<Search autoSubmit />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -438,6 +438,7 @@
|
||||||
"column_forbidden.body": "You do not have permission to access this page.",
|
"column_forbidden.body": "You do not have permission to access this page.",
|
||||||
"column_forbidden.title": "Forbidden",
|
"column_forbidden.title": "Forbidden",
|
||||||
"column.explorer": "Explorer",
|
"column.explorer": "Explorer",
|
||||||
|
"column.explorer.popular_accounts": "Popular Accounts",
|
||||||
"column.explorer.welcome_card.title": "Welcome to Explorer",
|
"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.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",
|
"column.explorer.nostr_card.title": "Nostr",
|
||||||
|
|
|
@ -22,6 +22,10 @@ const config: Config = {
|
||||||
boxShadow: ({ theme }) => ({
|
boxShadow: ({ theme }) => ({
|
||||||
'3xl': '0 25px 75px -15px rgba(0, 0, 0, 0.25)',
|
'3xl': '0 25px 75px -15px rgba(0, 0, 0, 0.25)',
|
||||||
'inset-ring': `inset 0 0 0 2px ${theme('colors.accent-blue')}`,
|
'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: {
|
fontSize: {
|
||||||
base: '0.9375rem',
|
base: '0.9375rem',
|
||||||
|
|
|
@ -7989,6 +7989,11 @@ svgo@^3.0.2:
|
||||||
csso "^5.0.5"
|
csso "^5.0.5"
|
||||||
picocolors "^1.0.0"
|
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:
|
symbol-tree@^3.2.4:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||||
|
|
Ładowanie…
Reference in New Issue