mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-31 12:14:30 +01:00
feature: Added Skeleton states
Added skeleton loading states to project mini card, project card, and project rows
This commit is contained in:
@@ -1,19 +1,23 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import Modal from 'src/Components/Modals/Modal/Modal';
|
||||
|
||||
export const ModalsDecorator = (Story: any) => {
|
||||
const onClose = () => { };
|
||||
return (
|
||||
<motion.div
|
||||
initial={false}
|
||||
className="w-screen fixed inset-0 overflow-x-hidden z-[2020]"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className='w-screen h-full fixed inset-0 py-32 md:py-64 flex flex-col justify-center items-center overflow-x-hidden overflow-y-hidden no-scrollbar'
|
||||
exit={{
|
||||
opacity: 0,
|
||||
transition: { ease: "easeInOut" },
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-screen h-full bg-gray-300 bg-opacity-50 absolute inset-0"
|
||||
onClick={onClose}
|
||||
></div>
|
||||
<Story />
|
||||
<AnimatePresence>
|
||||
<Modal onClose={onClose} >
|
||||
<Story onClose={onClose} />
|
||||
</Modal>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,28 @@ import { configure, addDecorator } from "@storybook/react";
|
||||
import { store } from "../src/redux/store";
|
||||
import React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
|
||||
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
ApolloProvider
|
||||
} from "@apollo/client";
|
||||
|
||||
|
||||
|
||||
import "react-multi-carousel/lib/styles.css";
|
||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
import 'react-loading-skeleton/dist/skeleton.css'
|
||||
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const client = new ApolloClient({
|
||||
uri: 'https://deploy-preview-2--makers-bolt-fun.netlify.app/.netlify/functions/graphql',
|
||||
cache: new InMemoryCache()
|
||||
});
|
||||
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
@@ -21,13 +37,14 @@ export const parameters = {
|
||||
};
|
||||
|
||||
addDecorator((S) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
<ApolloProvider client={client}>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<S />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</ApolloProvider>
|
||||
));
|
||||
|
||||
configure(require.context("../src", true, /\.stories\.ts$/), module);
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-loader-spinner": "^4.0.0",
|
||||
"react-loading-skeleton": "^3.0.2",
|
||||
"react-multi-carousel": "^2.6.5",
|
||||
"react-query": "^3.32.3",
|
||||
"react-redux": "^7.2.6",
|
||||
@@ -47620,6 +47621,14 @@
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-loading-skeleton": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.0.2.tgz",
|
||||
"integrity": "sha512-rlALwuZEcjazUDeIy3+fEhm20Uk9Yd/zZGeITU034K2ts5/yEf7RuZaV2FyrPWypIII4LAsFEo9WDTPKp6G0fQ==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-multi-carousel": {
|
||||
"version": "2.6.5",
|
||||
"integrity": "sha512-i5iuAm5XRT/h7uBL9/pGWeRsQXzqvjBrPVP1sobKgDKEvfZuKFpYp/alaQhTRM56Jtkb8jZpSqLn52Ku6jJbDg==",
|
||||
@@ -89771,6 +89780,12 @@
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-loading-skeleton": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.0.2.tgz",
|
||||
"integrity": "sha512-rlALwuZEcjazUDeIy3+fEhm20Uk9Yd/zZGeITU034K2ts5/yEf7RuZaV2FyrPWypIII4LAsFEo9WDTPKp6G0fQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-multi-carousel": {
|
||||
"version": "2.6.5",
|
||||
"integrity": "sha512-i5iuAm5XRT/h7uBL9/pGWeRsQXzqvjBrPVP1sobKgDKEvfZuKFpYp/alaQhTRM56Jtkb8jZpSqLn52Ku6jJbDg=="
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-loader-spinner": "^4.0.0",
|
||||
"react-loading-skeleton": "^3.0.2",
|
||||
"react-multi-carousel": "^2.6.5",
|
||||
"react-query": "^3.32.3",
|
||||
"react-redux": "^7.2.6",
|
||||
|
||||
70
src/Components/Badge/Badge.stories.tsx
Normal file
70
src/Components/Badge/Badge.stories.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Badge from './Badge';
|
||||
|
||||
export default {
|
||||
title: 'Shared/Badge',
|
||||
component: Badge,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof Badge>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof Badge> = (args) => <Badge {...args} >Badge</Badge>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
color: 'primary'
|
||||
}
|
||||
|
||||
export const SmallSize = Template.bind({});
|
||||
SmallSize.args = {
|
||||
color: 'primary',
|
||||
size: 'sm'
|
||||
}
|
||||
|
||||
export const MediumSize = Template.bind({});
|
||||
MediumSize.args = {
|
||||
color: 'primary',
|
||||
size: 'md'
|
||||
}
|
||||
|
||||
|
||||
export const LargeSize = Template.bind({});
|
||||
LargeSize.args = {
|
||||
color: 'primary',
|
||||
size: 'lg'
|
||||
}
|
||||
|
||||
export const Removable = Template.bind({});
|
||||
Removable.args = {
|
||||
color: 'primary',
|
||||
onRemove: () => { }
|
||||
}
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
isLoading: true
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Customized = Template.bind({});
|
||||
Customized.args = {
|
||||
href: "#",
|
||||
color: 'none',
|
||||
className: 'bg-red-500 text-white underline font-bold'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const ListTemplate: ComponentStory<typeof Badge> = (args) => <div className="flex gap-8">
|
||||
{Array(4).fill(0).map((_, idx) => <Badge key={idx} {...args} >Badge {idx + 1}</Badge>)}
|
||||
</div>
|
||||
|
||||
export const BadgesList = ListTemplate.bind({})
|
||||
72
src/Components/Badge/Badge.tsx
Normal file
72
src/Components/Badge/Badge.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import { IoMdCloseCircle } from "react-icons/io";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { wrapLink } from "src/utils/hoc";
|
||||
import { UnionToObjectKeys } from "src/utils/types/utils";
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
color?: 'primary' | 'gray' | 'none'
|
||||
size?: "sm" | 'md' | 'lg'
|
||||
shadow?: 'sm' | 'md' | 'lg' | 'none'
|
||||
className?: string;
|
||||
href?: string;
|
||||
onClick?: () => void
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
|
||||
const badgrColor: UnionToObjectKeys<Props, 'color'> = {
|
||||
none: '',
|
||||
primary: "bg-primary-600 border-0 text-white",
|
||||
gray: 'bg-gray-100 text-gray-900',
|
||||
}
|
||||
|
||||
const badgeSize: UnionToObjectKeys<Props, 'size'> = {
|
||||
sm: "px-8 py-4 text-body6",
|
||||
md: "px-16 py-8 text-body4",
|
||||
lg: "px-24 py-12 text-body3"
|
||||
}
|
||||
|
||||
const loadingBadgeSize: UnionToObjectKeys<Props, 'size'> = {
|
||||
sm: "w-48 h-24 text-body6",
|
||||
md: "w-64 h-32 text-body4",
|
||||
lg: "w-64 h-42 text-body3"
|
||||
}
|
||||
|
||||
|
||||
export default function Badge(
|
||||
{
|
||||
color = 'gray',
|
||||
size = 'md',
|
||||
className,
|
||||
href,
|
||||
shadow = 'sm',
|
||||
children,
|
||||
isLoading,
|
||||
onRemove,
|
||||
onClick
|
||||
}
|
||||
: PropsWithChildren<Props>) {
|
||||
|
||||
const classes = `
|
||||
rounded-48 shadow-${shadow} inline-block relative align-middle
|
||||
${badgrColor[color]}
|
||||
${badgeSize[size]}
|
||||
${className}
|
||||
${(onClick || href) && 'cursor-pointer'}
|
||||
`
|
||||
|
||||
if (isLoading)
|
||||
return <Skeleton width="6ch" className={`${loadingBadgeSize[size]} !rounded-48 leading-0`} />
|
||||
|
||||
|
||||
return (
|
||||
wrapLink(
|
||||
<span className={classes} onClick={onClick}>
|
||||
<span>{children}</span>
|
||||
{onRemove && <IoMdCloseCircle onClick={onRemove} className="ml-12 cursor-pointer" />}
|
||||
</span>
|
||||
, href)
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
|
||||
} as ComponentMeta<typeof Button>;
|
||||
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} >Click Me 🔥</Button>;
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} >Click Me !!</Button>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -18,17 +18,53 @@ Primary.args = {
|
||||
color: 'primary'
|
||||
}
|
||||
|
||||
export const Red = Template.bind({});
|
||||
Red.args = {
|
||||
color: 'red'
|
||||
}
|
||||
|
||||
export const Gray = Template.bind({});
|
||||
Gray.args = {
|
||||
color: 'gray'
|
||||
}
|
||||
|
||||
|
||||
export const OutlinePrimary = Template.bind({});
|
||||
OutlinePrimary.args = {
|
||||
color: 'primary',
|
||||
variant: 'outline'
|
||||
}
|
||||
|
||||
export const OutlineRed = Template.bind({});
|
||||
OutlineRed.args = {
|
||||
color: 'red',
|
||||
variant: 'outline'
|
||||
}
|
||||
|
||||
export const OutlineGray = Template.bind({});
|
||||
OutlineGray.args = {
|
||||
color: 'gray',
|
||||
variant: 'outline'
|
||||
}
|
||||
|
||||
export const SmallSize = Template.bind({});
|
||||
SmallSize.args = {
|
||||
color: 'primary',
|
||||
size: 'sm'
|
||||
}
|
||||
|
||||
export const MediumSize = Template.bind({});
|
||||
MediumSize.args = {
|
||||
color: 'primary',
|
||||
size: 'md'
|
||||
}
|
||||
|
||||
export const LargeSize = Template.bind({});
|
||||
LargeSize.args = {
|
||||
color: 'primary',
|
||||
size: 'lg'
|
||||
}
|
||||
|
||||
export const FullWidth = Template.bind({});
|
||||
FullWidth.args = {
|
||||
fullWidth: true
|
||||
@@ -37,4 +73,21 @@ FullWidth.args = {
|
||||
export const Link = Template.bind({});
|
||||
Link.args = {
|
||||
href: '#'
|
||||
}
|
||||
|
||||
export const DefaultLoading = Template.bind({});
|
||||
DefaultLoading.args = {
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
export const PrimaryLoading = Template.bind({});
|
||||
PrimaryLoading.args = {
|
||||
isLoading: true,
|
||||
color: 'primary'
|
||||
}
|
||||
|
||||
export const GrayLoading = Template.bind({});
|
||||
GrayLoading.args = {
|
||||
isLoading: true,
|
||||
color: 'gray'
|
||||
}
|
||||
@@ -1,32 +1,89 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import { wrapLink } from 'src/utils/hoc';
|
||||
import { UnionToObjectKeys } from 'src/utils/types/utils';
|
||||
// import Loading from '../Loading/Loading';
|
||||
|
||||
interface Props {
|
||||
color?: 'primary' | 'white' | 'gray'
|
||||
size?: 'md' | 'lg'
|
||||
color?: 'primary' | 'red' | 'white' | 'gray' | 'none',
|
||||
variant?: 'fill' | 'outline'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
children: ReactNode;
|
||||
href?: string;
|
||||
fullWidth?: boolean;
|
||||
onClick?: () => void;
|
||||
className?: string
|
||||
isLoading?: boolean;
|
||||
disableOnLoading?: boolean;
|
||||
[rest: string]: any;
|
||||
}
|
||||
|
||||
export default function Button(props: Props) {
|
||||
let classes = "btn inline-block";
|
||||
const btnStylesFill: UnionToObjectKeys<Props, 'color'> = {
|
||||
none: "",
|
||||
primary: "bg-primary-500 border-0 hover:bg-primary-400 active:bg-primary-600 text-white",
|
||||
gray: 'bg-gray-100 hover:bg-gray-200 text-gray-900 active:bg-gray-300',
|
||||
white: 'text-gray-900 bg-gray-25 hover:bg-gray-50',
|
||||
red: "bg-red-600 border-0 hover:bg-red-500 active:bg-red-700 text-white",
|
||||
}
|
||||
|
||||
if (props.color === 'primary') classes += ' btn-primary';
|
||||
else if (props.color === 'gray') classes += ' btn-gray';
|
||||
const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
|
||||
none: "",
|
||||
primary: "text-primary-600",
|
||||
gray: 'text-gray-700',
|
||||
white: 'text-gray-900',
|
||||
red: "text-red-500",
|
||||
}
|
||||
|
||||
if (props.size === 'md') classes += ' py-12 px-24';
|
||||
const baseBtnStyles: UnionToObjectKeys<Props, 'variant'> = {
|
||||
fill: " shadow-sm active:scale-95",
|
||||
outline: "bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 "
|
||||
}
|
||||
|
||||
if (props.fullWidth) classes += ' w-full'
|
||||
|
||||
// const loadingColor: UnionToObjectKeys<Props, 'color', ComponentProps<typeof Loading>['color']> = {
|
||||
// none: "white",
|
||||
// primary: "white",
|
||||
// gray: 'primary',
|
||||
// white: 'primary',
|
||||
// red: "white"
|
||||
// } as const;
|
||||
|
||||
const btnPadding: UnionToObjectKeys<Props, 'size'> = {
|
||||
sm: "py-8 px-12 text-body5",
|
||||
md: "py-12 px-24 text-body4",
|
||||
lg: 'py-12 px-36 text-body4'
|
||||
}
|
||||
|
||||
export default function Button({ color = 'white', variant = 'fill', isLoading, disableOnLoading = true, size = 'md', fullWidth, href, className, onClick, children, ...props }: Props) {
|
||||
|
||||
let classes = `
|
||||
inline-block font-sans rounded-lg font-regular border border-gray-300 hover:cursor-pointer
|
||||
${baseBtnStyles[variant]}
|
||||
${btnPadding[size]}
|
||||
${variant === 'fill' ? btnStylesFill[color] : btnStylesOutline[color]}
|
||||
${isLoading && disableOnLoading && 'bg-opacity-70 pointer-events-none'}
|
||||
`;
|
||||
|
||||
if (size === 'md') classes += ' py-12 px-24';
|
||||
if (size === 'lg')
|
||||
if (fullWidth) classes += ' w-full'
|
||||
|
||||
const handleClick = () => {
|
||||
if (props.onClick) props.onClick();
|
||||
if (isLoading && disableOnLoading) return;
|
||||
if (onClick) onClick();
|
||||
}
|
||||
|
||||
return (
|
||||
props.href ? <Link to={props.href} className={`${classes} ${props.className}`} onClick={handleClick}>{props.children}</Link> : <button className={`${classes} ${props.className}`} onClick={handleClick}>{props.children}</button>
|
||||
|
||||
return (
|
||||
wrapLink(
|
||||
<button
|
||||
type='button'
|
||||
className={`${classes} ${className}`}
|
||||
onClick={() => handleClick()}
|
||||
{...props}
|
||||
>
|
||||
{/* {isLoading ? <Loading color={loadingColor[color]} /> : children} */}
|
||||
{children}
|
||||
</button>
|
||||
, href)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { ALL_CATEGORIES_QUERY, ALL_CATEGORIES_QUERY_RES } from './query';
|
||||
import Badge from 'src/Components/Badge/Badge'
|
||||
|
||||
export default function Categories() {
|
||||
|
||||
@@ -9,12 +10,18 @@ export default function Categories() {
|
||||
|
||||
}
|
||||
|
||||
if (loading)
|
||||
return null;
|
||||
if (loading || !data)
|
||||
return <div className="flex gap-12 flex-wrap">
|
||||
{Array(5).fill(0).map((_, idx) =>
|
||||
<Badge key={idx} isLoading></Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="flex gap-12 flex-wrap">
|
||||
{data?.allCategories.map(category => <span key={category.id} className="chip hover:cursor-pointer hover:bg-gray-200" onClick={() => handleClick(category.id)}>{category.title}</span>)}
|
||||
{data?.allCategories.map(category =>
|
||||
<Badge key={category.id} onClick={() => handleClick(category.id)}>{category.title}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import Skeleton from 'react-loading-skeleton'
|
||||
|
||||
|
||||
export default function ProjectCardMiniSkeleton() {
|
||||
return (
|
||||
<div className="bg-gray-25 select-none px-16 py-16 flex w-[296px] gap-16 border border-gray-200 rounded-10 items-center" >
|
||||
<Skeleton width={80} height={80} containerClassName='flex-shrink-0' />
|
||||
<div className="justify-around items-start min-w-0">
|
||||
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap"><Skeleton width="15ch" /></p>
|
||||
<p className="text-body5 text-gray-600 font-light my-[5px]"><Skeleton width="10ch" /></p>
|
||||
<span className="font-light text-body5"> <Skeleton width="5ch"></Skeleton> </span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import mockData from 'src/api/mockData.json'
|
||||
|
||||
import ProjectCardMini from './ProjectCardMini';
|
||||
import ProjectCardMiniSkeleton from './ProjectCardMini.Skeleton';
|
||||
|
||||
|
||||
export default {
|
||||
@@ -19,3 +20,11 @@ Default.args = {
|
||||
|
||||
|
||||
|
||||
const SkeletonTemplate: ComponentStory<typeof ProjectCardMiniSkeleton> = (args) => <ProjectCardMiniSkeleton />;
|
||||
|
||||
export const LoadingState = SkeletonTemplate.bind({});
|
||||
LoadingState.args = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
21
src/pages/ExplorePage/ProjectsRow/ProjectsRow.Skeleton.tsx
Normal file
21
src/pages/ExplorePage/ProjectsRow/ProjectsRow.Skeleton.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import ProjectCardMiniSkeleton from "../ProjectCardMini/ProjectCardMini.Skeleton";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
|
||||
export default function ProjectsRowSkeleton() {
|
||||
|
||||
return (
|
||||
<div className='mb-48'>
|
||||
<h3 className="font-bolder text-body3 mb-24 px-32">
|
||||
<Skeleton width='10ch' />
|
||||
</h3>
|
||||
<div className="p-32 flex gap-20">
|
||||
{Array(5).fill(0).map((_, idx) => (
|
||||
<ProjectCardMiniSkeleton key={idx} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
import ProjectsRow from "../ProjectsRow/ProjectsRow";
|
||||
import ProjectsRowSkeleton from "../ProjectsRow/ProjectsRow.Skeleton";
|
||||
|
||||
import { MdLocalFireDepartment } from "react-icons/md";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { ALL_CATEGORIES_PROJECTS_QUERY, ALL_CATEGORIES_PROJECTS_RES } from "./query";
|
||||
@@ -9,7 +11,9 @@ export default function ProjectsSection() {
|
||||
|
||||
const { data, loading } = useQuery<ALL_CATEGORIES_PROJECTS_RES>(ALL_CATEGORIES_PROJECTS_QUERY);
|
||||
|
||||
if (loading || !data) return null;
|
||||
if (loading || !data) return <div className='mt-32 lg:mt-48'>
|
||||
{Array(3).fill(0).map((_, idx) => <ProjectsRowSkeleton key={idx} />)}
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div className='mt-32 lg:mt-48'>
|
||||
|
||||
82
src/pages/ProjectPage/ProjectCard/ProjectCard.Skeleton.tsx
Normal file
82
src/pages/ProjectPage/ProjectCard/ProjectCard.Skeleton.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { MdClose, } from 'react-icons/md';
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import { useAppSelector } from 'src/utils/hooks';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
}
|
||||
|
||||
export default function ProjectCardSkeleton({ onClose, direction, ...props }: Props) {
|
||||
|
||||
|
||||
|
||||
const { isMobileScreen } = useAppSelector(state => ({
|
||||
|
||||
isMobileScreen: state.theme.isMobileScreen
|
||||
}));
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && isMobileScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
<Skeleton height='100%' className='!leading-inherit' />
|
||||
<button className="w-[48px] h-[48px] bg-white z-10 absolute top-1/2 left-32 -translate-y-1/2 rounded-full hover:bg-gray-200 text-center" onClick={onClose}><MdClose className=' inline-block text-body2 lg:text-body1' /></button>
|
||||
</div>
|
||||
<div className="p-24">
|
||||
<div className="flex gap-24 items-center h-[93px]">
|
||||
<div className="flex-shrink-0 w-[93px] h-[93px] rounded-md overflow-hidden">
|
||||
<Skeleton height='100%' />
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-between self-stretch'>
|
||||
<h3 className="text-h3 font-regular"> <Skeleton width='13ch' /></h3>
|
||||
<span className="text-blue-400 font-regular text-body4" > <Skeleton width='6ch' /></span>
|
||||
<div className='flex gap-8'>
|
||||
<Badge size='sm' isLoading />
|
||||
<Badge size='sm' isLoading />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p className="mt-40 text-body4 leading-normal">
|
||||
|
||||
<Skeleton width='98%' />
|
||||
<Skeleton width='90%' />
|
||||
<Skeleton width='70%' />
|
||||
<Skeleton width='40%' />
|
||||
</p>
|
||||
|
||||
<div className="mt-40">
|
||||
<h3 className="text-h5 font-bold mb-16">Screenshots</h3>
|
||||
<div className="grid grid-cols-2 gap-12 justify-items-center md:gap-24">
|
||||
<div className="w-full relative pt-[56%]">
|
||||
<div className="absolute top-0 left-0 w-full h-full object-cover bg-gray-300 rounded-xl"></div>
|
||||
</div>
|
||||
<div className="w-full relative pt-[56%]">
|
||||
<div className="absolute top-0 left-0 w-full h-full object-cover bg-gray-300 rounded-xl"></div>
|
||||
</div>
|
||||
<div className="w-full relative pt-[56%]">
|
||||
<div className="absolute top-0 left-0 w-full h-full object-cover bg-gray-300 rounded-xl"></div>
|
||||
</div>
|
||||
<div className="w-full relative pt-[56%]">
|
||||
<div className="absolute top-0 left-0 w-full h-full object-cover bg-gray-300 rounded-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import ProjectCard from './ProjectCard';
|
||||
import ProjectCardSkeleton from './ProjectCard.Skeleton';
|
||||
|
||||
import { ModalsDecorator } from '.storybook/helpers'
|
||||
|
||||
@@ -15,3 +16,7 @@ const Template: ComponentStory<typeof ProjectCard> = (args) => <ProjectCard {...
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
|
||||
|
||||
const LoadingTemplate: ComponentStory<typeof ProjectCardSkeleton> = (args) => <ProjectCardSkeleton {...args} />;
|
||||
export const LoadingState = LoadingTemplate.bind({})
|
||||
@@ -11,6 +11,8 @@ import Button from 'src/Components/Button/Button';
|
||||
import { requestProvider } from 'webln';
|
||||
import { PROJECT_BY_ID_QUERY, PROJECT_BY_ID_RES, PROJECT_BY_ID_VARS } from './query'
|
||||
import { AiFillThunderbolt } from 'react-icons/ai';
|
||||
import ProjectCardSkeleton from './ProjectCard.Skeleton'
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
projectId: string
|
||||
@@ -39,7 +41,8 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
|
||||
|
||||
|
||||
|
||||
if (loading || !project) return <></>;
|
||||
if (loading || !project)
|
||||
return <ProjectCardSkeleton onClose={onClose} direction={direction} isPageModal={props.isPageModal} />;
|
||||
|
||||
const onConnectWallet = async () => {
|
||||
try {
|
||||
@@ -83,14 +86,8 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
<div
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && isMobileScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
|
||||
>
|
||||
<div className="relative h-[80px] lg:h-[152px]">
|
||||
<img className="w-full h-full object-cover" src={project.cover_image} alt="" />
|
||||
@@ -152,6 +149,6 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
|
||||
<Button color='gray' size='md' className="my-16" onClick={onClaim}>Claim 🖐</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from '../redux/store';
|
||||
|
||||
import 'react-multi-carousel/lib/styles.css';
|
||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import 'react-loading-skeleton/dist/skeleton.css'
|
||||
|
||||
|
||||
import {
|
||||
ApolloClient,
|
||||
|
||||
10
src/utils/hoc.tsx
Normal file
10
src/utils/hoc.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
||||
export function wrapLink(Component: JSX.Element, href: string | undefined, className?: string) {
|
||||
if (!href) return Component;
|
||||
|
||||
return <Link to={href} className={className}>
|
||||
{Component}
|
||||
</Link>
|
||||
}
|
||||
11
src/utils/types/utils.ts
Normal file
11
src/utils/types/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type UnionToObjectKeys<O, Key extends keyof O, Value = string> = { [EE in NonNullable<O[Key]> extends string ? NonNullable<O[Key]> : never]: Value }
|
||||
|
||||
export type Id<T> = {} & { [P in keyof T]: T[P] } // flatens out the types to make them more readable can be removed
|
||||
|
||||
export type RemoveCommonValues<T, TOmit> = {
|
||||
[P in keyof T]: TOmit extends Record<P, infer U> ? Exclude<T[P], U> : T[P]
|
||||
}
|
||||
|
||||
export type ValueOf<T> = Id<T[keyof T]>;
|
||||
|
||||
export type OmitId<T, Id extends string = 'id'> = Omit<T, Id>
|
||||
@@ -123,8 +123,13 @@ module.exports = {
|
||||
16: "16px",
|
||||
20: "20px",
|
||||
24: "24px",
|
||||
48: "48px",
|
||||
full: "50%",
|
||||
},
|
||||
lineHeight: {
|
||||
'inherit': "inherit",
|
||||
0: '0'
|
||||
},
|
||||
outline: {
|
||||
primary: ["2px solid #7B61FF", "1px"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user