Commit 60463634 authored by tom's avatar tom

skeletons for marketplace

parent e24ac42e
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import Marketplace from 'ui/pages/Marketplace';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const MarketplacePage: NextPage = () => { const MarketplacePage: NextPage = () => {
return ( return (
<Page> <Page>
......
/* eslint-disable max-len */
import type { MarketplaceAppOverview } from 'types/client/marketplace';
export const MARKETPLACE_APP: MarketplaceAppOverview = {
author: 'StubApp Inc.',
id: 'stub-app',
title: 'My cool app name',
logo: '',
categories: [
'Bridge',
],
shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.',
site: 'https://example.com',
description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.',
external: true,
url: 'https://example.com',
};
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Icon, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -14,6 +14,7 @@ interface Props extends MarketplaceAppPreview { ...@@ -14,6 +14,7 @@ interface Props extends MarketplaceAppPreview {
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
} }
const MarketplaceAppCard = ({ const MarketplaceAppCard = ({
...@@ -28,6 +29,7 @@ const MarketplaceAppCard = ({ ...@@ -28,6 +29,7 @@ const MarketplaceAppCard = ({
onInfoClick, onInfoClick,
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
isLoading,
}: Props) => { }: Props) => {
const categoriesLabel = categories.join(', '); const categoriesLabel = categories.join(', ');
...@@ -41,14 +43,15 @@ const MarketplaceAppCard = ({ ...@@ -41,14 +43,15 @@ const MarketplaceAppCard = ({
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const logoUrl = useColorModeValue(logo, logoDarkMode || logo); const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const moreButtonBgGradient = `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)`;
return ( return (
<LinkBox <LinkBox
_hover={{ _hover={{
boxShadow: 'md', boxShadow: isLoading ? 'none' : 'md',
}} }}
_focusWithin={{ _focusWithin={{
boxShadow: 'md', boxShadow: isLoading ? 'none' : 'md',
}} }}
borderRadius="md" borderRadius="md"
height="100%" height="100%"
...@@ -60,12 +63,13 @@ const MarketplaceAppCard = ({ ...@@ -60,12 +63,13 @@ const MarketplaceAppCard = ({
<Box <Box
display={{ base: 'grid', sm: 'block' }} display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }} gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }} gridTemplateRows={{ base: 'none', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }} gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }} gridColumnGap={{ base: 4, sm: 'none' }}
height="100%" height="100%"
> >
<Box <Skeleton
isLoaded={ !isLoading }
gridRow={{ base: '1 / 4', sm: 'auto' }} gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 } marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }} w={{ base: '64px', sm: '96px' }}
...@@ -76,17 +80,20 @@ const MarketplaceAppCard = ({ ...@@ -76,17 +80,20 @@ const MarketplaceAppCard = ({
justifyContent="center" justifyContent="center"
> >
<Image <Image
src={ logoUrl } src={ isLoading ? undefined : logoUrl }
alt={ `${ title } app icon` } alt={ `${ title } app icon` }
/> />
</Box> </Skeleton>
<Heading <Skeleton
isLoaded={ !isLoading }
gridColumn={{ base: 2, sm: 'auto' }} gridColumn={{ base: 2, sm: 'auto' }}
as="h3" as="h3"
marginBottom={ 2 } marginBottom={{ base: 0, sm: 2 }}
size={{ base: 'xs', sm: 'sm' }} fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold" fontWeight="semibold"
fontFamily="heading"
display="inline-block"
> >
<MarketplaceAppCardLink <MarketplaceAppCardLink
id={ id } id={ id }
...@@ -94,68 +101,74 @@ const MarketplaceAppCard = ({ ...@@ -94,68 +101,74 @@ const MarketplaceAppCard = ({
external={ external } external={ external }
title={ title } title={ title }
/> />
</Heading> </Skeleton>
<Text <Skeleton
marginBottom={ 2 } isLoaded={ !isLoading }
variant="secondary" marginBottom={{ base: 0, sm: 2 }}
color="text_secondary"
fontSize="xs" fontSize="xs"
> >
{ categoriesLabel } <span>{ categoriesLabel }</span>
</Text> </Skeleton>
<Text <Skeleton
isLoaded={ !isLoading }
fontSize={{ base: 'xs', sm: 'sm' }} fontSize={{ base: 'xs', sm: 'sm' }}
lineHeight="20px" lineHeight="20px"
noOfLines={ 4 } noOfLines={ 4 }
> >
{ shortDescription } { shortDescription }
</Text> </Skeleton>
<Box { !isLoading && (
position="absolute" <Box
right={{ base: 3, sm: '20px' }} position="absolute"
bottom={{ base: 3, sm: '20px' }} right={{ base: 3, sm: '20px' }}
paddingLeft={ 8 } bottom={{ base: 3, sm: '20px' }}
bgGradient={ `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)` } paddingLeft={ 8 }
> bgGradient={ moreButtonBgGradient }
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
maxW="100%"
overflow="hidden"
href="#"
onClick={ handleInfoClick }
> >
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
maxW="100%"
overflow="hidden"
href="#"
onClick={ handleInfoClick }
>
More More
<Icon <Icon
as={ northEastIcon } as={ northEastIcon }
marginLeft={ 1 } marginLeft={ 1 }
/> />
</Link> </Link>
</Box> </Box>
) }
<IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }} { !isLoading && (
_groupHover={{ display: 'block' }} <IconButton
position="absolute" display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
right={{ base: 3, sm: '10px' }} _groupHover={{ display: 'block' }}
top={{ base: 3, sm: '14px' }} position="absolute"
aria-label="Mark as favorite" right={{ base: 3, sm: '10px' }}
title="Mark as favorite" top={{ base: 3, sm: '14px' }}
variant="ghost" aria-label="Mark as favorite"
colorScheme="gray" title="Mark as favorite"
w={ 9 } variant="ghost"
h={ 8 } colorScheme="gray"
onClick={ handleFavoriteClick } w={ 9 }
icon={ isFavorite ? h={ 8 }
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> : onClick={ handleFavoriteClick }
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/> icon={ isFavorite ?
} <Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
/> <Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/>
}
/>
) }
</Box> </Box>
</LinkBox> </LinkBox>
); );
......
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react'; import { Box, Button, Icon, Menu, MenuButton, MenuList, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
...@@ -11,15 +11,28 @@ type Props = { ...@@ -11,15 +11,28 @@ type Props = {
categories: Array<string>; categories: Array<string>;
selectedCategoryId: string; selectedCategoryId: string;
onSelect: (category: string) => void; onSelect: (category: string) => void;
isLoading: boolean;
} }
const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => { const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories, isLoading }: Props) => {
const options = React.useMemo(() => ([ const options = React.useMemo(() => ([
MarketplaceCategory.FAVORITES, MarketplaceCategory.FAVORITES,
MarketplaceCategory.ALL, MarketplaceCategory.ALL,
...categories, ...categories,
]), [ categories ]); ]), [ categories ]);
if (isLoading) {
return (
<Skeleton
h="40px"
w={{ base: '100%', sm: '120px' }}
borderRadius="base"
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
/>
);
}
return ( return (
<Menu> <Menu>
<MenuButton <MenuButton
......
import { Grid, GridItem } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
...@@ -13,9 +13,10 @@ type Props = { ...@@ -13,9 +13,10 @@ type Props = {
onAppClick: (id: string) => void; onAppClick: (id: string) => void;
favoriteApps: Array<string>; favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
} }
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => { const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading }: Props) => {
return apps.length > 0 ? ( return apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -25,24 +26,22 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Pr ...@@ -25,24 +26,22 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Pr
autoRows="1fr" autoRows="1fr"
gap={{ base: '16px', sm: '24px' }} gap={{ base: '16px', sm: '24px' }}
> >
{ apps.map((app) => ( { apps.map((app, index) => (
<GridItem <MarketplaceAppCard
key={ app.id } key={ app.id + (isLoading ? index : '') }
> onInfoClick={ onAppClick }
<MarketplaceAppCard id={ app.id }
onInfoClick={ onAppClick } external={ app.external }
id={ app.id } url={ app.url }
external={ app.external } title={ app.title }
url={ app.url } logo={ app.logo }
title={ app.title } logoDarkMode={ app.logoDarkMode }
logo={ app.logo } shortDescription={ app.shortDescription }
logoDarkMode={ app.logoDarkMode } categories={ app.categories }
shortDescription={ app.shortDescription } isFavorite={ favoriteApps.includes(app.id) }
categories={ app.categories } onFavoriteClick={ onFavoriteClick }
isFavorite={ favoriteApps.includes(app.id) } isLoading={ isLoading }
onFavoriteClick={ onFavoriteClick } />
/>
</GridItem>
)) } )) }
</Grid> </Grid>
) : ( ) : (
......
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import MarketplaceAppCardSkeleton from './MarketplaceAppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
const MarketplaceListSkeleton = () => {
return (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ applicationStubs.map((app, index) => (
<GridItem
key={ index }
>
<MarketplaceAppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
export default MarketplaceListSkeleton;
...@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import useApiFetch from 'lib/hooks/useFetch'; import useApiFetch from 'lib/hooks/useFetch';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { MARKETPLACE_APP } from 'stubs/marketplace';
const favoriteAppsLocalStorageKey = 'favoriteApps'; const favoriteAppsLocalStorageKey = 'favoriteApps';
...@@ -44,11 +45,12 @@ export default function useMarketplace() { ...@@ -44,11 +45,12 @@ export default function useMarketplace() {
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]); const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>( const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ], [ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplaceConfigUrl || ''), async() => apiFetch(appConfig.marketplaceConfigUrl || ''),
{ {
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)), select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: Array(9).fill(MARKETPLACE_APP),
staleTime: Infinity, staleTime: Infinity,
}); });
...@@ -90,13 +92,13 @@ export default function useMarketplace() { ...@@ -90,13 +92,13 @@ export default function useMarketplace() {
}, [ ]); }, [ ]);
React.useEffect(() => { React.useEffect(() => {
if (!isLoading && !isError) { if (!isPlaceholderData && !isError) {
const isValidDefaultCategory = categories.includes(defaultCategoryId); const isValidDefaultCategory = categories.includes(defaultCategoryId);
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId); isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
} }
// run only when data is loaded // run only when data is loaded
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isLoading ]); }, [ isPlaceholderData ]);
React.useEffect(() => { React.useEffect(() => {
const query = _pickBy({ const query = _pickBy({
...@@ -118,7 +120,7 @@ export default function useMarketplace() { ...@@ -118,7 +120,7 @@ export default function useMarketplace() {
onCategoryChange: handleCategoryChange, onCategoryChange: handleCategoryChange,
filterQuery: debouncedFilterQuery, filterQuery: debouncedFilterQuery,
onSearchInputChange: setFilterQuery, onSearchInputChange: setFilterQuery,
isLoading, isPlaceholderData,
isError, isError,
error, error,
categories, categories,
...@@ -139,7 +141,7 @@ export default function useMarketplace() { ...@@ -139,7 +141,7 @@ export default function useMarketplace() {
handleCategoryChange, handleCategoryChange,
handleFavoriteClick, handleFavoriteClick,
isError, isError,
isLoading, isPlaceholderData,
showAppInfo, showAppInfo,
debouncedFilterQuery, debouncedFilterQuery,
]); ]);
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app/config'; import config from 'configs/app/config';
...@@ -6,14 +6,13 @@ import PlusIcon from 'icons/plus.svg'; ...@@ -6,14 +6,13 @@ import PlusIcon from 'icons/plus.svg';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'; import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import MarketplaceListSkeleton from 'ui/marketplace/MarketplaceListSkeleton';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
const Marketplace = () => { const Marketplace = () => {
const { const {
isLoading, isPlaceholderData,
isError, isError,
error, error,
selectedCategoryId, selectedCategoryId,
...@@ -45,24 +44,26 @@ const Marketplace = () => { ...@@ -45,24 +44,26 @@ const Marketplace = () => {
categories={ categories } categories={ categories }
selectedCategoryId={ selectedCategoryId } selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange } onSelect={ onCategoryChange }
isLoading={ isPlaceholderData }
/> />
<FilterInput <FilterInput
initialValue={ filterQuery } initialValue={ filterQuery }
onChange={ onSearchInputChange } onChange={ onSearchInputChange }
marginBottom={{ base: '4', lg: '6' }} marginBottom={{ base: '4', lg: '6' }}
w="100%"
placeholder="Find app" placeholder="Find app"
isLoading={ isPlaceholderData }
/> />
</Box> </Box>
{ isLoading ? <MarketplaceListSkeleton/> : ( <MarketplaceList
<MarketplaceList apps={ displayedApps }
apps={ displayedApps } onAppClick={ showAppInfo }
onAppClick={ showAppInfo } favoriteApps={ favoriteApps }
favoriteApps={ favoriteApps } onFavoriteClick={ onFavoriteClick }
onFavoriteClick={ onFavoriteClick } isLoading={ isPlaceholderData }
/> />
) }
{ selectedApp && ( { selectedApp && (
<MarketplaceAppModal <MarketplaceAppModal
...@@ -74,23 +75,28 @@ const Marketplace = () => { ...@@ -74,23 +75,28 @@ const Marketplace = () => {
) } ) }
{ config.marketplaceSubmitForm && ( { config.marketplaceSubmitForm && (
<Link <Skeleton
fontWeight="bold" isLoaded={ !isPlaceholderData }
display="inline-flex"
alignItems="baseline"
marginTop={{ base: 8, sm: 16 }} marginTop={{ base: 8, sm: 16 }}
href={ config.marketplaceSubmitForm } display="inline-block"
isExternal
> >
<Icon <Link
as={ PlusIcon } fontWeight="bold"
w={ 3 } display="inline-flex"
h={ 3 } alignItems="baseline"
mr={ 2 } href={ config.marketplaceSubmitForm }
/> isExternal
>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
Submit an App Submit an App
</Link> </Link>
</Skeleton>
) } ) }
</> </>
); );
......
import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react'; import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -11,9 +11,10 @@ type Props = { ...@@ -11,9 +11,10 @@ type Props = {
size?: 'xs' | 'sm' | 'md' | 'lg'; size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string; placeholder: string;
initialValue?: string; initialValue?: string;
isLoading?: boolean;
} }
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue }: Props) => { const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue, isLoading }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || ''); const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
...@@ -32,34 +33,38 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal ...@@ -32,34 +33,38 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
}, [ onChange ]); }, [ onChange ]);
return ( return (
<InputGroup <Skeleton
size={ size } isLoaded={ !isLoading }
className={ className } className={ className }
minW="250px" minW="250px"
> >
<InputLeftElement <InputGroup
pointerEvents="none" size={ size }
> >
<Icon as={ searchIcon } color={ iconColor }/> <InputLeftElement
</InputLeftElement> pointerEvents="none"
>
<Icon as={ searchIcon } color={ iconColor }/>
</InputLeftElement>
<Input <Input
ref={ inputRef } ref={ inputRef }
size={ size } size={ size }
value={ filterQuery } value={ filterQuery }
onChange={ handleFilterQueryChange } onChange={ handleFilterQueryChange }
placeholder={ placeholder } placeholder={ placeholder }
borderWidth="2px" borderWidth="2px"
textOverflow="ellipsis" textOverflow="ellipsis"
whiteSpace="nowrap" whiteSpace="nowrap"
/> />
{ filterQuery ? ( { filterQuery ? (
<InputRightElement> <InputRightElement>
<InputClearButton onClick={ handleFilterQueryClear }/> <InputClearButton onClick={ handleFilterQueryClear }/>
</InputRightElement> </InputRightElement>
) : null } ) : null }
</InputGroup> </InputGroup>
</Skeleton>
); );
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment