mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Add favorites to the marketplace.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.76 17.333a.603.603 0 0 1-.294-.075L9 14.798l-4.467 2.46a.606.606 0 0 1-.663-.051.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.854-5.21-3.616-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.301c.09-.08.2-.131.316-.149l4.994-.76 2.234-4.74a.65.65 0 0 1 .232-.269.61.61 0 0 1 .666 0c.1.065.18.158.233.269l2.233 4.74 4.994.76c.117.018.226.07.316.149a.66.66 0 0 1 .193.3.69.69 0 0 1-.16.678l-3.614 3.69.853 5.21a.692.692 0 0 1-.14.537.634.634 0 0 1-.216.173.605.605 0 0 1-.265.061Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 18 18" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z" fill="#4A5568"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+44
-6
@@ -1,16 +1,29 @@
|
||||
import { Box, Heading, Icon, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { AppItemPreview } from 'types/client/apps';
|
||||
|
||||
import northEastIcon from 'icons/arrows/north-east.svg';
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
import starOutlineIcon from 'icons/star_outline.svg';
|
||||
|
||||
interface Props extends AppItemPreview {
|
||||
onInfoClick: (id: string) => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
}
|
||||
|
||||
const AppCard = ({ id, title, logo, shortDescription, categories, onInfoClick }: Props) => {
|
||||
const AppCard = ({ id,
|
||||
title,
|
||||
logo,
|
||||
shortDescription,
|
||||
categories,
|
||||
onInfoClick,
|
||||
isFavorite,
|
||||
onFavoriteClick,
|
||||
}: Props) => {
|
||||
|
||||
const categoriesLabel = categories.map(c => c.name).join(', ');
|
||||
|
||||
const handleInfoClick = useCallback((event: MouseEvent) => {
|
||||
@@ -18,12 +31,23 @@ const AppCard = ({ id, title, logo, shortDescription, categories, onInfoClick }:
|
||||
onInfoClick(id);
|
||||
}, [ onInfoClick, id ]);
|
||||
|
||||
const handleFavoriteClick = useCallback(() => {
|
||||
onFavoriteClick(id, isFavorite);
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
return (
|
||||
<LinkBox
|
||||
_hover={{
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
_focusWithin={{
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
borderRadius="md"
|
||||
height="100%"
|
||||
padding={{ base: 3, sm: '20px' }}
|
||||
boxShadow={ `0 0 0 1px ${ useColorModeValue('var(--chakra-colors-gray-200)', 'var(--chakra-colors-gray-600)') }` }
|
||||
border="1px"
|
||||
borderColor={ useColorModeValue('gray.200', 'gray.600') }
|
||||
>
|
||||
<Box
|
||||
display={{ base: 'grid', sm: 'block' }}
|
||||
@@ -54,9 +78,6 @@ const AppCard = ({ id, title, logo, shortDescription, categories, onInfoClick }:
|
||||
>
|
||||
<LinkOverlay
|
||||
href="#"
|
||||
_hover={{
|
||||
textDecoration: 'underline',
|
||||
}}
|
||||
>
|
||||
{ title }
|
||||
</LinkOverlay>
|
||||
@@ -104,6 +125,23 @@ const AppCard = ({ id, title, logo, shortDescription, categories, onInfoClick }:
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
position="absolute"
|
||||
right={{ base: 3, sm: '20px' }}
|
||||
top={{ base: 3, sm: '20px' }}
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
|
||||
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</LinkBox>
|
||||
);
|
||||
|
||||
+45
-2
@@ -1,5 +1,5 @@
|
||||
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AppItemPreview } from 'types/client/apps';
|
||||
|
||||
@@ -7,12 +7,44 @@ import { apos } from 'lib/html-entities';
|
||||
import AppCard from 'ui/apps/AppCard';
|
||||
import EmptySearchResult from 'ui/apps/EmptySearchResult';
|
||||
|
||||
import AppModal from './AppModal';
|
||||
|
||||
type Props = {
|
||||
apps: Array<AppItemPreview>;
|
||||
onAppClick: (id: string) => void;
|
||||
displayedAppId: string | null;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
const AppList = ({ apps, onAppClick }: Props) => {
|
||||
function getFavoriteApps() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('favoriteApps') || '[]');
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
|
||||
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
|
||||
|
||||
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
|
||||
const favoriteApps = getFavoriteApps();
|
||||
|
||||
if (isFavorite) {
|
||||
const result = favoriteApps.filter((appId: string) => appId !== id);
|
||||
setFavoriteApps(result);
|
||||
localStorage.setItem('favoriteApps', JSON.stringify(result));
|
||||
} else {
|
||||
favoriteApps.push(id);
|
||||
localStorage.setItem('favoriteApps', JSON.stringify(favoriteApps));
|
||||
setFavoriteApps(favoriteApps);
|
||||
}
|
||||
}, [ ]);
|
||||
|
||||
useEffect(() => {
|
||||
setFavoriteApps(getFavoriteApps());
|
||||
}, [ ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VisuallyHidden>
|
||||
@@ -39,6 +71,8 @@ const AppList = ({ apps, onAppClick }: Props) => {
|
||||
logo={ app.logo }
|
||||
shortDescription={ app.shortDescription }
|
||||
categories={ app.categories }
|
||||
isFavorite={ favoriteApps.includes(app.id) }
|
||||
onFavoriteClick={ handleFavoriteClick }
|
||||
/>
|
||||
</GridItem>
|
||||
)) }
|
||||
@@ -46,6 +80,15 @@ const AppList = ({ apps, onAppClick }: Props) => {
|
||||
) : (
|
||||
<EmptySearchResult text={ `Couldn${ apos }t find an app that matches your filter query.` }/>
|
||||
) }
|
||||
|
||||
{ displayedAppId && (
|
||||
<AppModal
|
||||
id={ displayedAppId }
|
||||
onClose={ onModalClose }
|
||||
isFavorite={ favoriteApps.includes(displayedAppId) }
|
||||
onFavoriteClick={ handleFavoriteClick }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+14
-15
@@ -1,4 +1,4 @@
|
||||
import { LinkIcon, StarIcon } from '@chakra-ui/icons';
|
||||
import { LinkIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
|
||||
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
|
||||
@@ -12,26 +12,23 @@ import { TEMPORARY_DEMO_APPS } from 'data/apps';
|
||||
import ghIcon from 'icons/social/git.svg';
|
||||
import tgIcon from 'icons/social/telega.svg';
|
||||
import twIcon from 'icons/social/tweet.svg';
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
import starOutlineIcon from 'icons/star_outline.svg';
|
||||
import { nbsp } from 'lib/html-entities';
|
||||
|
||||
type Props = {
|
||||
id: string | null;
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
}
|
||||
|
||||
const AppModal = ({
|
||||
id,
|
||||
onClose,
|
||||
isFavorite,
|
||||
onFavoriteClick,
|
||||
}: Props) => {
|
||||
const handleFavorite = useCallback(() => {
|
||||
// TODO: implement
|
||||
}, []);
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
author,
|
||||
@@ -45,8 +42,6 @@ const AppModal = ({
|
||||
categories,
|
||||
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview;
|
||||
|
||||
const isFavorite = false;
|
||||
|
||||
const socialLinks = [
|
||||
Boolean(telegram) && {
|
||||
icon: tgIcon,
|
||||
@@ -62,6 +57,10 @@ const AppModal = ({
|
||||
},
|
||||
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>;
|
||||
|
||||
const handleFavoriteClick = useCallback(() => {
|
||||
onFavoriteClick(id, isFavorite);
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={ Boolean(id) }
|
||||
@@ -135,10 +134,10 @@ const AppModal = ({
|
||||
colorScheme="gray"
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavorite }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<Icon as={ StarIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
|
||||
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 }/> }
|
||||
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
|
||||
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/> }
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+5
-5
@@ -5,7 +5,6 @@ import type { AppItemOverview } from 'types/client/apps';
|
||||
|
||||
import { TEMPORARY_DEMO_APPS } from 'data/apps';
|
||||
import AppList from 'ui/apps/AppList';
|
||||
import AppModal from 'ui/apps/AppModal';
|
||||
import FilterInput from 'ui/shared/FilterInput';
|
||||
|
||||
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ]
|
||||
@@ -34,10 +33,11 @@ const Apps = () => {
|
||||
return (
|
||||
<>
|
||||
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
|
||||
<AppList apps={ displayedApps } onAppClick={ showAppInfo }/>
|
||||
<AppModal
|
||||
id={ displayedAppId }
|
||||
onClose={ clearDisplayedAppId }
|
||||
<AppList
|
||||
apps={ displayedApps }
|
||||
onAppClick={ showAppInfo }
|
||||
displayedAppId={ displayedAppId }
|
||||
onModalClose={ clearDisplayedAppId }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user