mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-02 05:04:29 +01:00
feat: project details modal
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Categories from './Categories';
|
||||
|
||||
|
||||
export default {
|
||||
title: 'Projects/Explore Page/Categories',
|
||||
component: Categories,
|
||||
|
||||
} as ComponentMeta<typeof Categories>;
|
||||
|
||||
const Template: ComponentStory<typeof Categories> = (args) => <Categories />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
68
src/features/Projects/Components/Categories/Categories.tsx
Normal file
68
src/features/Projects/Components/Categories/Categories.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAllCategoriesQuery } from 'src/graphql';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { useCarousel } from 'src/utils/hooks';
|
||||
|
||||
const colors = [
|
||||
'#FDF2F8',
|
||||
'#F5F3FF',
|
||||
'#FEFCE8',
|
||||
'#F0FDF4',
|
||||
'#EFF6FF',
|
||||
'#FFFBEB',
|
||||
'#FEF2F2',
|
||||
'#FDF2F8',
|
||||
'#FFF7ED',
|
||||
'#F1F5F9'
|
||||
]
|
||||
|
||||
export default function Categories() {
|
||||
|
||||
const { viewportRef, scrollSlides, canScrollNext, canScrollPrev, isClickAllowed } = useCarousel({
|
||||
align: 'start', slidesToScroll: 2,
|
||||
containScroll: "trimSnaps",
|
||||
})
|
||||
const { data, loading } = useAllCategoriesQuery();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
if (loading || !data)
|
||||
return <div className="flex gap-12">
|
||||
{Array(5).fill(0).map((_, idx) =>
|
||||
<div
|
||||
key={idx}
|
||||
className=' block p-16 rounded-16 bg-gray-100 active:scale-90 transition-transform'
|
||||
>
|
||||
<span className="opacity-0">category</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="relative group">
|
||||
<div className="overflow-hidden" ref={viewportRef}>
|
||||
<div className="select-none w-full flex gap-16">
|
||||
{data?.categoryList?.filter(c => c !== null).map((category, idx) =>
|
||||
<button
|
||||
key={category!.id}
|
||||
onClick={() => { }}
|
||||
className='min-w-max block p-16 rounded-16 hover:bg-gray-100 active:bg-gray-200 active:scale-90 transition-transform'
|
||||
style={{ backgroundColor: colors[idx % colors.length] }}
|
||||
>{category!.icon} {category!.name}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button className={`absolute text-body6 w-[28px] aspect-square flex justify-center items-center left-0 -translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full bg-white text-gray-400 opacity-0 ${canScrollPrev && 'group-hover:opacity-100'} active:scale-90 transition-opacity border border-gray-200 shadow-md`} onClick={() => scrollSlides(-1)}>
|
||||
<FaChevronLeft />
|
||||
</button>
|
||||
<button className={`absolute text-body6 w-[28px] aspect-square flex justify-center items-center right-0 translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full bg-white text-gray-400 opacity-0 ${canScrollNext && 'group-hover:opacity-100'} active:scale-90 transition-opacity border border-gray-200 shadow-md`} onClick={() => scrollSlides(1)}>
|
||||
<FaChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
query AllCategories {
|
||||
categoryList {
|
||||
id
|
||||
name
|
||||
icon
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ export default function ProjectCardMini({ project, onClick }: Props) {
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
>
|
||||
<img src={project?.logo?.[0]['thumbnails']['large'].url} alt={project?.project ?? ''} draggable="false" className="flex-shrink-0 w-64 h-64 bg-gray-200 border-0 rounded-full hover:cursor-pointer"></img>
|
||||
<img src={project?.logo?.[0]['thumbnails']['large'].url} alt={project?.title ?? ''} draggable="false" className="flex-shrink-0 w-64 h-64 bg-gray-200 border-0 rounded-full hover:cursor-pointer"></img>
|
||||
<div className="justify-around items-start min-w-0 flex-1 hover:cursor-pointer"
|
||||
>
|
||||
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap">{project?.category}</p>
|
||||
<p className="text-body4 w-full font-bold overflow-ellipsis overflow-hidden whitespace-nowrap">{project?.title}</p>
|
||||
{/* <p className="text-body5 text-gray-600 font-light my-[5px]">{project?.category.title}</p> */}
|
||||
{/* <span className="chip-small bg-warning-50 text-yellow-700 font-light text-body5 py-[3px] px-10"> <MdLocalFireDepartment className='inline-block text-fire transform text-body4 align-middle' /> {numberFormatter(project?.votes_count)} </span> */}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useExplorePageQuery } from 'src/graphql';
|
||||
import HeaderImage from './HeaderImage/HeaderImage';
|
||||
import ProjectsGrid from './ProjectsGrid/ProjectsGrid';
|
||||
import { Helmet } from "react-helmet";
|
||||
import Categories from '../../Components/Categories/Categories';
|
||||
|
||||
export default function ExplorePage() {
|
||||
|
||||
@@ -38,6 +39,7 @@ export default function ExplorePage() {
|
||||
img={data?.getCategory.cover_image!}
|
||||
apps_count={data?.getCategory.apps_count!}
|
||||
/> */}
|
||||
<Categories />
|
||||
<div className="mt-40">
|
||||
<ProjectsGrid
|
||||
isLoading={loading}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
query ExplorePage($page: JSON, $pageSize: JSON) {
|
||||
projects(_page: $page, _page_size: $pageSize) {
|
||||
id
|
||||
project
|
||||
title
|
||||
category
|
||||
logo
|
||||
yearFounded
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Claim_CopySignatureCard from './Claim_CopySignatureCard';
|
||||
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Claim/Copy Signature Card',
|
||||
component: Claim_CopySignatureCard,
|
||||
|
||||
decorators: [ModalsDecorator]
|
||||
} as ComponentMeta<typeof Claim_CopySignatureCard>;
|
||||
|
||||
const Template: ComponentStory<typeof Claim_CopySignatureCard> = (args) => <Claim_CopySignatureCard {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Direction, replaceModal } from 'src/redux/features/modals.slice';
|
||||
import { useAppDispatch, useAppSelector } from 'src/utils/hooks';
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import CopyToClipboard from 'src/Components/CopyToClipboard/CopyToClipboard'
|
||||
import { useCallback } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
|
||||
export default function Claim_CopySignatureCard({ onClose, direction, ...props }: ModalCard) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { projectName, image } = useAppSelector(state => ({ projectName: state.project.project?.title, image: state.project.project?.thumbnail_image, }))
|
||||
|
||||
const generatedHash = '0x000330RaaSt302440zxc327jjiaswf19987183345pRReuBNksbaaueee'
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
dispatch(replaceModal({
|
||||
Modal: 'Claim_SubmittedCard',
|
||||
direction: Direction.NEXT
|
||||
}))
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[343px] p-24 rounded-xl relative"
|
||||
>
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold'>Claim this project</h2>
|
||||
<div className="flex justify-center my-32">
|
||||
<img
|
||||
src={image}
|
||||
className='w-80 h-80 object-cover rounded-2xl'
|
||||
alt="" />
|
||||
</div>
|
||||
<p className="text-body4 text-center px-16">
|
||||
Good job! Now paste this on the webpage
|
||||
<a className="font-bold" href="www.projectname.com/"
|
||||
target='_blank' rel='noreferrer'
|
||||
> www.projectname.com/</a>
|
||||
</p>
|
||||
<div className="input-wrapper mt-32">
|
||||
<input
|
||||
className="input-text overflow-ellipsis"
|
||||
type='text'
|
||||
value={generatedHash}
|
||||
/>
|
||||
|
||||
<CopyToClipboard text={generatedHash} />
|
||||
</div>
|
||||
<div className="mt-32">
|
||||
<button className='btn btn-primary w-full' onClick={handleNext}>Submit for review</button>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Claim_FundWithdrawCard from './Claim_FundWithdrawCard';
|
||||
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Claim/Fund Withdraw Card',
|
||||
component: Claim_FundWithdrawCard,
|
||||
|
||||
decorators: [ModalsDecorator]
|
||||
} as ComponentMeta<typeof Claim_FundWithdrawCard>;
|
||||
|
||||
const Template: ComponentStory<typeof Claim_FundWithdrawCard> = (args) => <Claim_FundWithdrawCard {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { motion } from 'framer-motion'
|
||||
// import { useAppDispatch } from '../../utils/hooks';
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
|
||||
export default function Claim_FundWithdrawCard({ onClose, direction, ...props }: ModalCard) {
|
||||
|
||||
//const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[260px] py-16 px-24 rounded-xl relative"
|
||||
>
|
||||
<div className="flex justify-center my-16">
|
||||
<img
|
||||
src={'assets/icons/lightning-small.svg'}
|
||||
className='w-48 h-48 object-cover rounded-full'
|
||||
alt="" />
|
||||
</div>
|
||||
<p className="text-h4 text-center font-bold">
|
||||
2,220 sats
|
||||
</p>
|
||||
<p className="text-body4 text-center text-gray-400">
|
||||
2.78$
|
||||
</p>
|
||||
<div className="mt-16 flex flex-col gap-8">
|
||||
<button className='btn btn-primary w-full shadow-xs' >Fund</button>
|
||||
<button className='btn border w-full shadow-xs' >Withdraw</button>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Claim_GenerateSignatureCard from './Claim_GenerateSignatureCard';
|
||||
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Claim/Generate Signature Card',
|
||||
component: Claim_GenerateSignatureCard,
|
||||
decorators: [ModalsDecorator]
|
||||
} as ComponentMeta<typeof Claim_GenerateSignatureCard>;
|
||||
|
||||
const Template: ComponentStory<typeof Claim_GenerateSignatureCard> = (args) => <Claim_GenerateSignatureCard {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Direction, replaceModal } from 'src/redux/features/modals.slice';
|
||||
import { useAppDispatch, useAppSelector } from 'src/utils/hooks';
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
|
||||
export default function Claim_GenerateSignatureCard({ onClose, direction, ...props }: ModalCard) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { projectName, image } = useAppSelector(state => ({ projectName: state.project.project?.title, image: state.project.project?.thumbnail_image, }))
|
||||
|
||||
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
dispatch(replaceModal({
|
||||
Modal: 'Claim_CopySignatureCard',
|
||||
direction: Direction.NEXT
|
||||
}))
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
// const timeout = setTimeout(handleNext, 3000)
|
||||
// return () => clearTimeout(timeout)
|
||||
}, [handleNext])
|
||||
|
||||
//const onCopy = () => {
|
||||
// // Copy to Clipboard
|
||||
// setTimeout(handleNext, 2000)
|
||||
//}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[343px] p-24 rounded-xl relative"
|
||||
>
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold'>Claim this project</h2>
|
||||
<div className="flex justify-center my-32">
|
||||
<img
|
||||
src={image}
|
||||
className='w-80 h-80 object-cover rounded-2xl'
|
||||
alt="" />
|
||||
</div>
|
||||
<p className="text-body4 text-center px-16">
|
||||
To claim ownership of <span className="font-bold">{projectName}</span> and its tips, you need to sign a message and paste this on the project website so we can verify you are the real ownership
|
||||
</p>
|
||||
<div className="mt-32">
|
||||
<button className='btn btn-primary w-full' onClick={handleNext}>Generate Signature</button>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import Claim_SubmittedCard from './Claim_SubmittedCard';
|
||||
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Claim/Submitted Card',
|
||||
component: Claim_SubmittedCard,
|
||||
|
||||
decorators: [ModalsDecorator]
|
||||
} as ComponentMeta<typeof Claim_SubmittedCard>;
|
||||
|
||||
const Template: ComponentStory<typeof Claim_SubmittedCard> = (args) => <Claim_SubmittedCard {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
|
||||
export default function Claim_SubmittedCard({ onClose, direction, ...props }: ModalCard) {
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[343px] p-24 rounded-xl relative"
|
||||
|
||||
>
|
||||
<IoClose
|
||||
className='absolute text-body2 top-24 right-24 hover:cursor-pointer'
|
||||
onClick={onClose} />
|
||||
|
||||
<h2 className='text-h5 font-bold'>Submitted For Review</h2>
|
||||
<div className="flex justify-center my-32">
|
||||
<img
|
||||
src="assets/icons/flag-icon.svg"
|
||||
className='w-80 h-80'
|
||||
alt="success" />
|
||||
</div>
|
||||
<p className="text-body4 text-center">
|
||||
Great work! your claim to <span className="font-bold">Application Name</span> has been submitted for review.
|
||||
<br />
|
||||
Check back soon to see if it was successful.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
import { lazyModal } from "src/utils/helperFunctions";
|
||||
|
||||
export const { LazyComponent: Claim_CopySignatureCard } = lazyModal(() => import('./Claim_CopySignatureCard'))
|
||||
export const { LazyComponent: Claim_FundWithdrawCard } = lazyModal(() => import('./Claim_FundWithdrawCard'))
|
||||
export const { LazyComponent: Claim_GenerateSignatureCard } = lazyModal(() => import('./Claim_GenerateSignatureCard'))
|
||||
export const { LazyComponent: Claim_SubmittedCard } = lazyModal(() => import('./Claim_SubmittedCard'))
|
||||
@@ -0,0 +1,130 @@
|
||||
import linkifyHtml from 'linkifyjs/lib/linkify-html'
|
||||
import { useState } from 'react'
|
||||
import { MdLocalFireDepartment } from 'react-icons/md'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Lightbox from 'src/Components/Lightbox/Lightbox'
|
||||
import { ProjectDetailsQuery, ProjectLaunchStatusEnum, ProjectPermissionEnum, } from 'src/graphql'
|
||||
import { openModal } from 'src/redux/features/modals.slice'
|
||||
import { setVoteAmount } from 'src/redux/features/vote.slice'
|
||||
import { numberFormatter } from 'src/utils/helperFunctions'
|
||||
import { useAppDispatch } from 'src/utils/hooks'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
import LinksCard from '../LinksCard/LinksCard'
|
||||
|
||||
interface Props {
|
||||
project: Pick<ProjectDetailsQuery['getProject'],
|
||||
| "id"
|
||||
| "cover_image"
|
||||
| "thumbnail_image"
|
||||
| "title"
|
||||
| "category"
|
||||
| "permissions"
|
||||
| "launch_status"
|
||||
| "description"
|
||||
| "screenshots"
|
||||
| "tagline"
|
||||
| "website"
|
||||
| "votes_count"
|
||||
| 'discord'
|
||||
| 'website'
|
||||
| 'github'
|
||||
| 'twitter'
|
||||
| 'slack'
|
||||
| 'telegram'
|
||||
>
|
||||
}
|
||||
|
||||
|
||||
export default function AboutCard({ project }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [screenshotsOpen, setScreenshotsOpen] = useState(-1);
|
||||
|
||||
const onVote = (votes?: number) => {
|
||||
dispatch(setVoteAmount(votes ?? 10));
|
||||
dispatch(openModal({
|
||||
Modal: 'VoteCard', props: {
|
||||
projectId: project.id,
|
||||
title: project.title,
|
||||
initVotes: votes
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
const canEdit = project.permissions.includes(ProjectPermissionEnum.UpdateInfo);
|
||||
|
||||
return (
|
||||
<Card defaultPadding={false} onlyMd>
|
||||
{/* Cover Image */}
|
||||
<div className="hidden md:block relative rounded-t-12 md:rounded-t-16 h-[120px] lg:h-[160px]">
|
||||
<img className="w-full h-full object-cover rounded-12 md:rounded-0 md:rounded-t-16" src={project.cover_image} alt="" />
|
||||
<div className="absolute top-16 md:top-24 left-24 flex gap-8 bg-gray-800 bg-opacity-60 text-white rounded-48 py-4 px-12 text-body6 font-medium">
|
||||
{project.launch_status === ProjectLaunchStatusEnum.Launched && `🚀 Launched`}
|
||||
{project.launch_status === ProjectLaunchStatusEnum.Wip && `🔧 WIP`}
|
||||
</div>
|
||||
<div className="absolute left-24 bottom-0 translate-y-1/2 w-[108px] aspect-square">
|
||||
<img className="w-full h-full border-2 border-white rounded-24" src={project.thumbnail_image} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:p-24 md:pt-0 flex flex-col gap-24">
|
||||
{/* Title & Basic Info */}
|
||||
<div className="flex flex-col gap-24 relative">
|
||||
<div className="flex flex-wrap justify-end items-center gap-16 min-h-[40px] mt-12">
|
||||
{canEdit && <Button size="sm" color="gray" href={createRoute({ type: "edit-project", id: project.id })}>Edit Project</Button>}
|
||||
<Button size="sm" variant='outline' color='gray' className='hidden md:block hover:!text-red-500 hover:!border-red-200 hover:!bg-red-50' onClick={() => onVote()}>
|
||||
<MdLocalFireDepartment />{<span className="align-middle w-[4ch]"> {numberFormatter(project.votes_count)}</span>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-col gap-8 items-start justify-between -mt-12'>
|
||||
<a href={project.website} target='_blank' rel="noreferrer"><h3 className="text-body1 font-bold">{project.title}</h3></a>
|
||||
<p className="text-body4 text-gray-600">{project.tagline}</p>
|
||||
<div>
|
||||
<span className="font-medium text-body5 text-gray-900">{project.category.icon} {project.category.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" fullWidth variant='outline' color='gray' className='md:hidden hover:!text-red-500 hover:!border-red-200 hover:!bg-red-50' onClick={() => onVote()}>
|
||||
<MdLocalFireDepartment />{<span className="align-middle w-[4ch]"> {numberFormatter(project.votes_count)}</span>}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<LinksCard links={project} />
|
||||
</div>
|
||||
|
||||
{/* About */}
|
||||
<div>
|
||||
<div className="text-body4 text-gray-600 leading-normal whitespace-pre-line" dangerouslySetInnerHTML={{
|
||||
__html: linkifyHtml(project.description, {
|
||||
className: ' text-blue-500 underline',
|
||||
defaultProtocol: 'https',
|
||||
target: "_blank",
|
||||
rel: 'noreferrer'
|
||||
})
|
||||
}}></div>
|
||||
|
||||
</div>
|
||||
{project.screenshots.length > 0 && <>
|
||||
<div className="">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 justify-items-center">
|
||||
{project.screenshots.slice(0, 4).map((screenshot, idx) => <div
|
||||
key={idx}
|
||||
className="w-full relative pt-[56%] cursor-pointer bg-gray-100 border rounded-10 overflow-hidden"
|
||||
onClick={() => setScreenshotsOpen(idx)}
|
||||
>
|
||||
<img src={screenshot} className="absolute top-0 left-0 w-full h-full object-cover" alt='' />
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
<Lightbox
|
||||
images={project.screenshots}
|
||||
isOpen={screenshotsOpen !== -1}
|
||||
initOpenIndex={screenshotsOpen}
|
||||
onClose={() => setScreenshotsOpen(-1)}
|
||||
/>
|
||||
</>}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import Badge from 'src/Components/Badge/Badge'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import { ProjectDetailsQuery } from 'src/graphql'
|
||||
|
||||
|
||||
interface Props {
|
||||
capabilities: ProjectDetailsQuery['getProject']['capabilities']
|
||||
}
|
||||
|
||||
|
||||
export default function CapabilitiesCard({ capabilities }: Props) {
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<p className="text-body6 max-md:uppercase max-md:text-gray-400 md:text-body2 font-bold">🦾 Capabilities</p>
|
||||
<div className="mt-16">
|
||||
{capabilities.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No capabilities added</p>
|
||||
</>}
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{capabilities.map(cap => <Badge key={cap.id} size='sm'>{cap.icon} {cap.title}</Badge>)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import { Project } from 'src/graphql'
|
||||
import { FaDiscord, } from 'react-icons/fa';
|
||||
import { FiGithub, FiGlobe, FiTwitter } from 'react-icons/fi';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { NotificationsService, } from 'src/services'
|
||||
|
||||
interface Props {
|
||||
links: Pick<Project, 'discord' | 'website' | 'github' | 'twitter' | 'slack' | 'telegram'>
|
||||
}
|
||||
|
||||
export default function LinksCard({ links }: Props) {
|
||||
const linksList = [
|
||||
{
|
||||
value: links.discord,
|
||||
text: links.discord,
|
||||
icon: FaDiscord,
|
||||
colors: "bg-violet-100 text-violet-900",
|
||||
},
|
||||
{
|
||||
value: links.website,
|
||||
text: links.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, ""),
|
||||
icon: FiGlobe,
|
||||
colors: "bg-gray-100 text-gray-900",
|
||||
url: links.website
|
||||
},
|
||||
{
|
||||
value: links.twitter,
|
||||
text: links.twitter,
|
||||
icon: FiTwitter,
|
||||
colors: "bg-blue-100 text-blue-500",
|
||||
url: links.twitter
|
||||
},
|
||||
{
|
||||
value: links.github,
|
||||
text: links.github,
|
||||
icon: FiGithub,
|
||||
colors: "bg-pink-100 text-pink-600",
|
||||
url: links.github
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<p className="text-body2 font-bold mb-16 hidden md:block">🔗 Links</p>
|
||||
<div className="">
|
||||
{linksList.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No links added</p>
|
||||
</>}
|
||||
<div className="flex flex-wrap gap-16">
|
||||
{linksList.filter(link => !!link.value).map((link, idx) =>
|
||||
(link.url ? <a
|
||||
key={idx}
|
||||
href={link.url!}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
target='_blank'
|
||||
rel="noreferrer">
|
||||
<link.icon className="scale-125" />
|
||||
</a>
|
||||
:
|
||||
<CopyToClipboard
|
||||
text={link.value!}
|
||||
onCopy={() => NotificationsService.info(" Copied to clipboard", { icon: "📋" })}
|
||||
>
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => { }}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
>
|
||||
<link.icon className="scale-125" />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import Badge from 'src/Components/Badge/Badge'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
|
||||
import { sortMembersByRole } from 'src/features/Projects/utils/helperFunctions'
|
||||
import { ProjectDetailsQuery } from 'src/graphql'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
|
||||
|
||||
interface Props {
|
||||
members: ProjectDetailsQuery['getProject']['members']
|
||||
recruit_roles: ProjectDetailsQuery['getProject']['recruit_roles']
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function MakersCard({ members, recruit_roles }: Props) {
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<p className="text-body6 max-md:uppercase max-md:text-gray-400 md:text-body2 font-bold">👾 Makers</p>
|
||||
<div className="mt-16">
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{members.length === 0 && <p className="text-body4 text-gray-500">Not listed</p>}
|
||||
{sortMembersByRole(members).map(m => <Link key={m.user.id} to={createRoute({ type: "profile", id: m.user.id, username: m.user.name })}>
|
||||
<Avatar
|
||||
width={40}
|
||||
src={m.user.avatar}
|
||||
renderTooltip={() => <div className='bg-white px-12 py-8 border border-gray-200 rounded-12 flex flex-wrap gap-12 shadow-lg'>
|
||||
<Avatar width={48} src={m.user.avatar} />
|
||||
<div className='overflow-hidden'>
|
||||
<p className={`text-black font-medium overflow-hidden text-ellipsis`}>{m.user.name}</p>
|
||||
<p className={`text-body6 text-gray-600`}>{m.user.jobTitle}</p>
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
</Link>)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mt-24">Open roles</p>
|
||||
<div className="mt-8">
|
||||
{recruit_roles.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No open roles for now</p>
|
||||
</>}
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{recruit_roles.map(role => <Badge key={role.id} size='sm'>{role.icon} {role.title}</Badge>)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Badge from 'src/Components/Badge/Badge'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import { ProjectDetailsQuery } from 'src/graphql'
|
||||
|
||||
|
||||
interface Props {
|
||||
recruit_roles: ProjectDetailsQuery['getProject']['recruit_roles']
|
||||
}
|
||||
|
||||
|
||||
export default function OpenRolesCard({ recruit_roles }: Props) {
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<p className="text-body2 font-bold">👀 Open roles</p>
|
||||
<div className="mt-16">
|
||||
{recruit_roles.length === 0 && <>
|
||||
<p className="text-gray-700 text-body4">No open roles for now</p>
|
||||
</>}
|
||||
<div className="flex flex-wrap gap-16">
|
||||
{recruit_roles.map(role => <Badge key={role.id} size='sm'>{role.icon} {role.title}</Badge>)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import SimilarProjectsCard from './SimilarProjectsCard';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Project Page/Similar Projects Card',
|
||||
component: SimilarProjectsCard,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof SimilarProjectsCard>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof SimilarProjectsCard> = (args) => <div className="max-w-[326px]"><SimilarProjectsCard {...args as any} ></SimilarProjectsCard></div>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
import { Link } from 'react-router-dom'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
|
||||
import { User, useSimilarProjectsQuery } from 'src/graphql'
|
||||
import { createRoute } from 'src/utils/routing'
|
||||
|
||||
interface Props {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default function SimilarProjectsCard({ id }: Props) {
|
||||
|
||||
const query = useSimilarProjectsQuery({ variables: { projectId: id } })
|
||||
|
||||
if (query.loading) return null;
|
||||
|
||||
if (query.data?.similarProjects.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card onlyMd>
|
||||
<h3 className="text-body2 font-bolder">🚀 Similar projects</h3>
|
||||
<ul className='flex flex-col'>
|
||||
{query.data?.similarProjects.map(project => {
|
||||
return <Link key={project.id} to={createRoute({ type: "project", tag: project.hashtag })} className="md:border-b py-16 last-of-type:border-b-0 last-of-type:pb-0">
|
||||
<li className="flex items-center gap-12">
|
||||
<img className='w-48 aspect-square rounded-12 border border-gray-100' alt='' src={project.thumbnail_image} />
|
||||
<div className='overflow-hidden'>
|
||||
<p className="text-body4 text-gray-800 font-medium whitespace-nowrap overflow-hidden text-ellipsis">{project.title}</p>
|
||||
<p className="text-body5 text-gray-500">{project.category.icon} {project.category.title}</p>
|
||||
</div>
|
||||
</li>
|
||||
</Link>
|
||||
})}
|
||||
</ul>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
query ProjectDetails($projectsId: String) {
|
||||
projects(id: $projectsId) {
|
||||
id
|
||||
title
|
||||
dead
|
||||
createdAt
|
||||
companyName
|
||||
category
|
||||
categoryList {
|
||||
name
|
||||
}
|
||||
description
|
||||
discord
|
||||
endDate
|
||||
twitter
|
||||
updatedAt
|
||||
watchers
|
||||
website
|
||||
yearFounded
|
||||
telegram
|
||||
subcategory
|
||||
stars
|
||||
repository
|
||||
openSource
|
||||
logo
|
||||
linkedIn
|
||||
license
|
||||
language
|
||||
forks
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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 { useMediaQuery } from 'src/utils/hooks';
|
||||
import { MEDIA_QUERIES } from 'src/utils/theme';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
}
|
||||
|
||||
export default function ProjectDetailsCardSkeleton({ onClose, direction, ...props }: Props) {
|
||||
|
||||
|
||||
|
||||
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className={`modal-card max-w-[676px] ${props.isPageModal && !isMdScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
|
||||
>
|
||||
<div className="relative h-[100px] lg:h-[80px]">
|
||||
<Skeleton height='100%' className='!leading-inherit' />
|
||||
<button className="w-32 h-32 bg-gray-600 bg-opacity-80 text-white absolute top-24 right-24 rounded-full hover:bg-gray-800 text-center" onClick={onClose}><MdClose className=' inline-block' /></button>
|
||||
</div>
|
||||
<div className="p-24 flex flex-col gap-24">
|
||||
<div className="flex flex-col mt-[-80px] md:flex-row md:mt-0 gap-24 items-start relative">
|
||||
<div className="flex-shrink-0 w-[108px] h-[108px] ">
|
||||
<Skeleton height='100%' className='rounded-24 border-2 border-white' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-8 items-start justify-between'>
|
||||
<h3 className="text-body1 font-bold"><Skeleton width='13ch' /></h3>
|
||||
<p className="text-body4 text-gray-600"><Skeleton width='30ch' /></p>
|
||||
<div>
|
||||
<span className="font-medium text-body4 text-gray-600"><Skeleton width='10ch' /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 w-full md:w-auto md:flex ml-auto gap-16 self-stretch">
|
||||
<Button fullWidth variant='outline' color='gray' className='!px-8'>
|
||||
<p className='opacity-0'>votes</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-body4 leading-normal">
|
||||
<Skeleton width='98%' />
|
||||
<Skeleton width='90%' />
|
||||
<Skeleton width='70%' />
|
||||
<Skeleton width='40%' />
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-16">
|
||||
<Skeleton width='40px' height='40px' className='rounded-full' />
|
||||
<Skeleton width='40px' height='40px' className='rounded-full' />
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 justify-items-center">
|
||||
{
|
||||
Array(4).fill(0).map((_, idx) => <div key={idx} className="w-full relative pt-[56%] cursor-pointer bg-gray-200 shadow-sm rounded-10 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-full object-cover"></div>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center h-[46px]"></div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ProjectDetailsCard from './ProjectDetailsCard';
|
||||
import ProjectDetailsCardSkeleton from './ProjectDetailsCard.Skeleton';
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Project Page/Project Details Modal',
|
||||
component: ProjectDetailsCard,
|
||||
decorators: [ModalsDecorator],
|
||||
} as ComponentMeta<typeof ProjectDetailsCard>;
|
||||
|
||||
const Template: ComponentStory<typeof ProjectDetailsCard> = (args) => <ProjectDetailsCard {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
projectId: 1,
|
||||
isPageModal: true
|
||||
}
|
||||
|
||||
|
||||
|
||||
const LoadingTemplate: ComponentStory<typeof ProjectDetailsCardSkeleton> = (args) => <ProjectDetailsCardSkeleton {...args} />;
|
||||
export const LoadingState = LoadingTemplate.bind({
|
||||
isPageModal: true
|
||||
})
|
||||
@@ -0,0 +1,275 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MdLocalFireDepartment } from 'react-icons/md';
|
||||
import { ModalCard } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import { useAppDispatch, useAppSelector, useMediaQuery } from 'src/utils/hooks';
|
||||
import { openModal, scheduleModal } from 'src/redux/features/modals.slice';
|
||||
import { setProject } from 'src/redux/features/project.slice';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import ProjectCardSkeleton from './ProjectDetailsCard.Skeleton'
|
||||
// import VoteButton from 'src/features/Projects/pages/ProjectPage/VoteButton/VoteButton';
|
||||
import { NotificationsService, Wallet_Service } from 'src/services'
|
||||
import { ProjectLaunchStatusEnum, ProjectPermissionEnum, useProjectDetailsQuery } from 'src/graphql';
|
||||
import Lightbox from 'src/Components/Lightbox/Lightbox'
|
||||
import linkifyHtml from 'linkify-html';
|
||||
import ErrorMessage from 'src/Components/Errors/ErrorMessage/ErrorMessage';
|
||||
import { setVoteAmount } from 'src/redux/features/vote.slice';
|
||||
import { numberFormatter } from 'src/utils/helperFunctions';
|
||||
import { MEDIA_QUERIES } from 'src/utils/theme';
|
||||
import { FaDiscord, } from 'react-icons/fa';
|
||||
import { FiEdit2, FiGithub, FiGlobe, FiTwitter } from 'react-icons/fi';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Badge from 'src/Components/Badge/Badge';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createRoute } from 'src/utils/routing';
|
||||
import { IoMdClose } from 'react-icons/io';
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export default function ProjectDetailsCard({ direction, projectId, ...props }: Props) {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [screenshotsOpen, setScreenshotsOpen] = useState(-1);
|
||||
|
||||
|
||||
const { isWalletConnected } = useAppSelector(state => ({
|
||||
isWalletConnected: state.wallet.isConnected,
|
||||
}));
|
||||
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
|
||||
|
||||
const { data, loading, error } = useProjectDetailsQuery({
|
||||
variables: {
|
||||
projectsId: projectId!,
|
||||
},
|
||||
onCompleted: data => {
|
||||
dispatch(setProject((data.projects?.[0] as any) ?? null))
|
||||
},
|
||||
onError: () => {
|
||||
dispatch(setProject(null));
|
||||
},
|
||||
skip: !Boolean(projectId)
|
||||
});
|
||||
|
||||
|
||||
|
||||
const closeModal = () => {
|
||||
props.onClose?.();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (error)
|
||||
return <div
|
||||
className={`modal-card max-w-[768px] ${props.isPageModal && !isMdScreen && 'rounded-0 w-full min-h-screen'}`}
|
||||
>
|
||||
<div className="p-64">
|
||||
<ErrorMessage type='fetching' message='Something Wrong happened while fetching project details, please try refreshing the page' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if (loading)
|
||||
return <ProjectCardSkeleton onClose={closeModal} direction={direction} isPageModal={props.isPageModal} />;
|
||||
|
||||
|
||||
const project = data?.projects?.[0];
|
||||
|
||||
if (!project) return <p>404</p>
|
||||
|
||||
const links = [
|
||||
{
|
||||
value: project.discord,
|
||||
text: project.discord,
|
||||
icon: FaDiscord,
|
||||
colors: "bg-violet-100 text-violet-900",
|
||||
},
|
||||
{
|
||||
value: project.website,
|
||||
text: project.website?.replace(/(^\w+:|^)\/\//, '').replace(/\/$/, ""),
|
||||
icon: FiGlobe,
|
||||
colors: "bg-gray-100 text-gray-900",
|
||||
url: project.website
|
||||
},
|
||||
{
|
||||
value: project.twitter,
|
||||
text: project.twitter,
|
||||
icon: FiTwitter,
|
||||
colors: "bg-blue-100 text-blue-500",
|
||||
url: project.twitter
|
||||
},
|
||||
{
|
||||
value: project.github,
|
||||
text: project.github,
|
||||
icon: FiGithub,
|
||||
colors: "bg-pink-100 text-pink-600",
|
||||
url: project.github
|
||||
},
|
||||
];
|
||||
|
||||
const onVote = (votes?: number) => {
|
||||
dispatch(setVoteAmount(votes ?? 10));
|
||||
// dispatch(openModal({
|
||||
// Modal: 'VoteCard', props: {
|
||||
// projectId: project.id,
|
||||
// title: project.title,
|
||||
// initVotes: votes
|
||||
// }
|
||||
// }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`modal-card max-w-[676px] ${(props.isPageModal && !isMdScreen) && '!rounded-0 w-full min-h-screen'}`}
|
||||
>
|
||||
{/* Cover Image */}
|
||||
<div className="relative h-[120px] lg:h-[80px]">
|
||||
<img className="w-full h-full object-cover" src={project.cover_image} alt="" />
|
||||
<div className="absolute w-full px-16 md:px-24 top-16 md:top-24 flex justify-between items-center">
|
||||
<div className="flex gap-8 bg-gray-800 bg-opacity-60 text-white rounded-48 py-4 px-12 text-body6 font-medium">
|
||||
{project.launch_status === ProjectLaunchStatusEnum.Launched && `🚀 Launched`}
|
||||
{project.launch_status === ProjectLaunchStatusEnum.Wip && `🔧 WIP`}
|
||||
</div>
|
||||
<div className="flex gap-8">
|
||||
{project.permissions.includes(ProjectPermissionEnum.UpdateInfo) &&
|
||||
<Link className="w-32 h-32 bg-gray-800 bg-opacity-60 text-white rounded-full hover:bg-opacity-40 text-center flex flex-col justify-center items-center" onClick={() => props.onClose?.()} to={createRoute({ type: "edit-project", id: project.id })}><FiEdit2 /></Link>}
|
||||
<button className="w-32 h-32 bg-gray-800 bg-opacity-60 text-white rounded-full hover:bg-opacity-40 text-center flex flex-col justify-center items-center" onClick={closeModal}><IoMdClose className=' inline-block' /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-24 flex flex-col gap-24">
|
||||
|
||||
{/* Title & Basic Info */}
|
||||
<div className="flex flex-col mt-[-80px] md:flex-row md:mt-0 gap-24 md:items-center relative">
|
||||
<div className="flex-shrink-0 w-[108px] h-[108px]">
|
||||
<img className="w-full h-full border-2 border-white rounded-24" src={project.thumbnail_image} alt="" />
|
||||
</div>
|
||||
<div className='flex flex-col gap-8 items-start justify-between'>
|
||||
<a href={project.website} target='_blank' rel="noreferrer"><h3 className="text-body1 font-bold">{project.title}</h3></a>
|
||||
<p className="text-body4 text-gray-600">{project.tagline}</p>
|
||||
<div>
|
||||
<span className="font-medium text-body4 text-gray-600">{project.category.icon} {project.category.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-full md:w-auto md:flex ml-auto gap-16 self-stretch">
|
||||
{/* <Button color='primary' size='md' className=" my-16" href={project.website} newTab >Visit <BsJoystick /></Button> */}
|
||||
{/* <VoteButton onVote={onVote} /> */}
|
||||
{/* <VoteButton fullWidth votes={project.votes_count} direction='vertical' onVote={onVote} /> */}
|
||||
{/* {isWalletConnected ?
|
||||
:
|
||||
<Button onClick={onConnectWallet} size='md' className="border border-gray-200 bg-gray-100 hover:bg-gray-50 active:bg-gray-100 my-16"><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Vote</Button>
|
||||
} */}
|
||||
<Button fullWidth variant='outline' color='gray' className='!px-8' onClick={() => onVote()}>
|
||||
<div className="flex justify-center items-center gap-8 md:flex-col ">
|
||||
<MdLocalFireDepartment />{<span className="align-middle w-[4ch]"> {numberFormatter(project.votes_count)}</span>}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* About */}
|
||||
<div>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">About</p>
|
||||
<div className=" text-body4 text-gray-600 leading-normal whitespace-pre-line" dangerouslySetInnerHTML={{
|
||||
__html: linkifyHtml(project.description, {
|
||||
className: ' text-blue-500 underline',
|
||||
defaultProtocol: 'https',
|
||||
target: "_blank",
|
||||
rel: 'noreferrer'
|
||||
})
|
||||
}}></div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="mt-16 flex flex-wrap gap-16">
|
||||
{links.filter(link => !!link.value).map((link, idx) =>
|
||||
(link.url ? <a
|
||||
key={idx}
|
||||
href={link.url!}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
target='_blank'
|
||||
rel="noreferrer">
|
||||
<link.icon className="scale-125" />
|
||||
</a>
|
||||
:
|
||||
<CopyToClipboard
|
||||
text={link.value!}
|
||||
onCopy={() => NotificationsService.info(" Copied to clipboard", { icon: "📋" })}
|
||||
>
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => { }}
|
||||
className={`w-40 aspect-square rounded-full flex justify-center items-center ${link.colors}`}
|
||||
>
|
||||
<link.icon className="scale-125" />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{project.screenshots.length > 0 && <>
|
||||
<div className="">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 justify-items-center">
|
||||
{project.screenshots.slice(0, 4).map((screenshot, idx) => <div
|
||||
key={idx}
|
||||
className="w-full relative pt-[56%] cursor-pointer bg-gray-100 border rounded-10 overflow-hidden"
|
||||
onClick={() => setScreenshotsOpen(idx)}
|
||||
>
|
||||
<img src={screenshot} className="absolute top-0 left-0 w-full h-full object-cover" alt='' />
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
<Lightbox
|
||||
images={project.screenshots}
|
||||
isOpen={screenshotsOpen !== -1}
|
||||
initOpenIndex={screenshotsOpen}
|
||||
onClose={() => setScreenshotsOpen(-1)}
|
||||
/>
|
||||
</>}
|
||||
|
||||
{project.capabilities.length > 0 &&
|
||||
<div>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">CAPABILITIES</p>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{project.capabilities.map(cap => <Badge key={cap.id} size='sm'>{cap.icon} {cap.title}</Badge>)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{project.members.length > 0 &&
|
||||
<div className='relative'>
|
||||
<p className="text-body6 uppercase font-medium text-gray-400 mb-8">MAKERS</p>
|
||||
<div className="flex flex-wrap gap-8">
|
||||
{sortMembersByRole(project.members).map(m => <Link key={m.user.id} to={createRoute({ type: "profile", id: m.user.id, username: m.user.name })}>
|
||||
<Avatar
|
||||
width={40}
|
||||
src={m.user.avatar}
|
||||
renderTooltip={() => <div className='bg-white px-12 py-8 border border-gray-200 rounded-12 flex flex-wrap gap-12 shadow-lg relative z-10'>
|
||||
<Avatar width={48} src={m.user.avatar} />
|
||||
<div className='overflow-hidden'>
|
||||
<p className={`text-black font-medium overflow-hidden text-ellipsis`}>{m.user.name}</p>
|
||||
<p className={`text-body6 text-gray-600`}>{m.user.jobTitle}</p>
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
</Link>)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Button color='white' fullWidth href={createRoute({ type: "project", tag: project.hashtag })} onClick={props.onClose}>View project details</Button>
|
||||
|
||||
{/* <div className="text-center">
|
||||
<h3 className="text-body4 font-regular">Are you the creator of this project</h3>
|
||||
<Button
|
||||
color='gray'
|
||||
size='md'
|
||||
className="my-16"
|
||||
href={`https://airtable.com/shr67F20KG9Gdok6d?prefill_app_name=${project.title}&prefill_app_link=${project.website}`}
|
||||
newTab
|
||||
// onClick={onClaim}
|
||||
>Claim 🖐</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
import ProjectDetailsCardSkeleton from './ProjectDetailsCard.Skeleton'
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
|
||||
|
||||
export const { LazyComponent: ProjectDetailsCard, preload: projectDetailsCardPreload } = lazyModal(() => import('./ProjectDetailsCard'), ProjectDetailsCardSkeleton)
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import VoteButton from './VoteButton';
|
||||
import { centerDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Project Page/Vote Button',
|
||||
component: VoteButton,
|
||||
decorators: [
|
||||
centerDecorator
|
||||
]
|
||||
} as ComponentMeta<typeof VoteButton>;
|
||||
|
||||
const Template: ComponentStory<typeof VoteButton> = (args) => <VoteButton onVote={() => { }} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { MdLocalFireDepartment } from 'react-icons/md'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import { useAppSelector, usePressHolder } from 'src/utils/hooks'
|
||||
import _throttle from 'lodash.throttle'
|
||||
import { ComponentProps, useRef, useState } from 'react'
|
||||
import styles from './vote-button-style.module.css'
|
||||
import { random, randomItem } from 'src/utils/helperFunctions'
|
||||
|
||||
|
||||
interface Particle {
|
||||
id: string,
|
||||
offsetX: number,
|
||||
color: '#ff6a00' | '#ff7717' | '#ff6217' | '#ff8217' | '#ff5717'
|
||||
animation: 'fly-spark-1' | 'fly-spark-2',
|
||||
animationSpeed: number,
|
||||
scale: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
onVote: (Vote: number) => void
|
||||
} & Omit<ComponentProps<typeof Button>, 'children'>
|
||||
|
||||
export default function VoteButton({ onVote = () => { }, ...props }: Props) {
|
||||
const [voteCnt, setVoteCnt] = useState(0)
|
||||
const voteCntRef = useRef(0);
|
||||
const [sparks, setSparks] = useState<Particle[]>([]);
|
||||
const [wasActive, setWasActive] = useState(false);
|
||||
|
||||
const isMobileScreen = useAppSelector(s => s.ui.isMobileScreen)
|
||||
|
||||
|
||||
const { onPressDown, onPressUp } = usePressHolder(_throttle(() => {
|
||||
const _incStep = (Math.ceil((voteCnt + 1) / 10) + 1) ** 2 * 10;
|
||||
setVoteCnt(s => {
|
||||
const newValue = s + _incStep;
|
||||
voteCntRef.current = newValue;
|
||||
return newValue;
|
||||
})
|
||||
|
||||
const newSpark = {
|
||||
id: Math.random().toString(),
|
||||
offsetX: random(1, 99),
|
||||
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1) as any,
|
||||
animationSpeed: randomItem(1, 1.5, 2),
|
||||
color: randomItem('#ff6a00', '#ff7717', '#ff6217', '#ff8217', '#ff5717'),
|
||||
scale: random(1.2, 2.2)
|
||||
} as const;
|
||||
|
||||
// if on mobile screen, reduce number of sparks particles to 60%
|
||||
if (!isMobileScreen || Math.random() > .4) {
|
||||
setSparks(oldSparks => [...oldSparks, newSpark])
|
||||
setTimeout(() => {
|
||||
setSparks(s => {
|
||||
return s.filter(spark => spark.id !== newSpark.id)
|
||||
})
|
||||
|
||||
}, newSpark.animationSpeed * 1000)
|
||||
}
|
||||
|
||||
}, 100), 100);
|
||||
|
||||
const handlePressDown = () => {
|
||||
setWasActive(true);
|
||||
onPressDown();
|
||||
}
|
||||
|
||||
const handlePressUp = (event?: any) => {
|
||||
if (!wasActive) return;
|
||||
|
||||
setWasActive(false);
|
||||
if (event?.preventDefault) event.preventDefault();
|
||||
onPressUp();
|
||||
if (voteCnt === 0)
|
||||
onVote(10);
|
||||
else
|
||||
setTimeout(() => {
|
||||
setSparks([]);
|
||||
onVote(voteCntRef.current);
|
||||
setVoteCnt(0);
|
||||
voteCntRef.current = 0;
|
||||
}, 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onMouseDown={handlePressDown}
|
||||
onMouseUp={handlePressUp}
|
||||
onMouseLeave={handlePressUp}
|
||||
|
||||
onTouchStart={handlePressDown}
|
||||
onTouchEnd={handlePressUp}
|
||||
size='md'
|
||||
color='none'
|
||||
className={`${styles.vote_button} border relative 100 my-16 noselect`}
|
||||
style={{
|
||||
"--scale": voteCnt,
|
||||
} as any}
|
||||
{...props}
|
||||
>
|
||||
Hold To Vote !!! <MdLocalFireDepartment className='text-fire' />
|
||||
|
||||
<span
|
||||
className={styles.vote_counter}
|
||||
>{voteCnt}</span>
|
||||
|
||||
<div
|
||||
className={styles.spark}
|
||||
style={{
|
||||
"--offsetX": 23,
|
||||
"--animationSpeed": 3,
|
||||
"--scale": 1,
|
||||
"animationIterationCount": 'infinite',
|
||||
"animationName": styles.fly_spark_1,
|
||||
"animationDelay": '1.1s',
|
||||
color: '#ff6a00'
|
||||
} as any}
|
||||
><MdLocalFireDepartment className='' /></div>
|
||||
<div
|
||||
className={styles.spark}
|
||||
style={{
|
||||
"--offsetX": 50,
|
||||
"--animationSpeed": 2.2,
|
||||
"--scale": 1,
|
||||
"animationIterationCount": 'infinite',
|
||||
"animationName": styles.fly_spark_2,
|
||||
"animationDelay": '0.4s',
|
||||
color: '#ff6a00'
|
||||
} as any}
|
||||
><MdLocalFireDepartment className='' /></div>
|
||||
<div
|
||||
className={styles.spark}
|
||||
style={{
|
||||
"--offsetX": 70,
|
||||
"--animationSpeed": 2.5,
|
||||
"--scale": 1,
|
||||
"animationIterationCount": 'infinite',
|
||||
"animationName": styles.fly_spark_1,
|
||||
color: '#ff6a00'
|
||||
} as any}
|
||||
><MdLocalFireDepartment className='' /></div>
|
||||
{sparks.map(spark =>
|
||||
<div
|
||||
key={spark.id}
|
||||
className={styles.spark}
|
||||
style={{
|
||||
"--offsetX": spark.offsetX,
|
||||
"--animationSpeed": spark.animationSpeed,
|
||||
"--scale": spark.scale,
|
||||
"animationName": spark.animation,
|
||||
"color": spark.color
|
||||
} as any}
|
||||
><MdLocalFireDepartment className='' /></div>)
|
||||
}
|
||||
</Button>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
.vote_button {
|
||||
--scale: 0;
|
||||
transition: background-color 1s;
|
||||
background-color: hsl(25, 100%, max(calc((95 - var(--scale) / 4) * 1%), 63%));
|
||||
}
|
||||
|
||||
.vote_counter {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 110%;
|
||||
color: hsl(25, 100%, 50%);
|
||||
font-weight: bold;
|
||||
font-size: 21px;
|
||||
will-change: transform;
|
||||
opacity: min(calc(var(--scale) * 1), 1);
|
||||
transform: translate(-50%, max(calc(-1px * var(--scale) / 10), -30px))
|
||||
scale(calc(1 + min(var(--scale) / 150, 2)));
|
||||
text-shadow: 0 0 4px hsl(25, 100%, 50%);
|
||||
}
|
||||
|
||||
.spark {
|
||||
position: absolute;
|
||||
bottom: 46%;
|
||||
left: calc(var(--offsetX) * 1%);
|
||||
transform: scale(var(--scale));
|
||||
opacity: 0;
|
||||
will-change: transform;
|
||||
|
||||
animation-name: fly-spark-1;
|
||||
animation-duration: calc(var(--animationSpeed) * 1s);
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
filter: drop-shadow(0 0 4px);
|
||||
}
|
||||
|
||||
@keyframes fly_spark_1 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(var(--scale));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
33% {
|
||||
transform: translate(12px, -70px) scale(var(--scale));
|
||||
}
|
||||
|
||||
66% {
|
||||
transform: translate(0, -140px) scale(var(--scale));
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(6px, -200px) scale(var(--scale));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fly_spark_2 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(var(--scale));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-10px, -80px) scale(var(--scale));
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(-4px, -140px) scale(var(--scale));
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-6px, -160px) scale(var(--scale));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import VoteCard from './VoteCard';
|
||||
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Projects/Project Page/Vote Card',
|
||||
component: VoteCard,
|
||||
|
||||
decorators: [ModalsDecorator]
|
||||
} as ComponentMeta<typeof VoteCard>;
|
||||
|
||||
const Template: ComponentStory<typeof VoteCard> = (args) => <VoteCard {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
126
src/features/Projects/pages/ProjectPage/VoteCard/VoteCard.tsx
Normal file
126
src/features/Projects/pages/ProjectPage/VoteCard/VoteCard.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import React, { FormEvent, useState } from 'react';
|
||||
import { AiFillThunderbolt } from 'react-icons/ai'
|
||||
import { IoClose } from 'react-icons/io5'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
import { PaymentStatus, useVote } from 'src/utils/hooks';
|
||||
import Confetti from "react-confetti";
|
||||
import { useWindowSize } from '@react-hookz/web';
|
||||
import { Vote_Item_Type } from 'src/graphql';
|
||||
import IconButton from 'src/Components/IconButton/IconButton';
|
||||
|
||||
const defaultOptions = [
|
||||
{ text: '100 sat', value: 100 },
|
||||
{ text: '1k sat', value: 1000 },
|
||||
{ text: '10k sats', value: 10000 },
|
||||
]
|
||||
|
||||
|
||||
interface Props extends ModalCard {
|
||||
projectId: number;
|
||||
title?: string;
|
||||
initVotes?: number;
|
||||
}
|
||||
|
||||
export default function VoteCard({ onClose, direction, projectId, initVotes, ...props }: Props) {
|
||||
const { width, height } = useWindowSize()
|
||||
|
||||
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState(10);
|
||||
const [voteAmount, setVoteAmount] = useState<number>(initVotes ?? 10);
|
||||
const { vote, paymentStatus } = useVote({
|
||||
itemId: projectId,
|
||||
itemType: Vote_Item_Type.Project
|
||||
})
|
||||
|
||||
|
||||
|
||||
const onChangeInput = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedOption(-1);
|
||||
setVoteAmount(Number(event.target.value));
|
||||
};
|
||||
|
||||
const onSelectOption = (idx: number) => {
|
||||
setSelectedOption(idx);
|
||||
setVoteAmount(defaultOptions[idx].value);
|
||||
}
|
||||
|
||||
const requestPayment = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
vote(voteAmount, {
|
||||
onSuccess: () => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 4000);
|
||||
},
|
||||
onError: () => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 4000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[343px] p-24 rounded-xl relative"
|
||||
>
|
||||
<div className="flex items-start gap-12">
|
||||
<h2 className='text-h5 font-bold'>Vote for {props.title ?? "project"}</h2>
|
||||
<IconButton onClick={onClose} >
|
||||
<IoClose className='text-body2' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<form onSubmit={requestPayment} className="mt-32 ">
|
||||
<label className="block text-gray-700 text-body4 mb-2 ">
|
||||
Enter Amount
|
||||
</label>
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
className={` input-text input-removed-arrows`}
|
||||
value={voteAmount} onChange={onChangeInput}
|
||||
type="number"
|
||||
placeholder="e.g 5 sats"
|
||||
autoFocus
|
||||
/>
|
||||
<p className='px-16 shrink-0 self-center text-primary-400'>
|
||||
Sats
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex mt-16 justify-between">
|
||||
{defaultOptions.map((option, idx) =>
|
||||
<button
|
||||
type='button'
|
||||
key={idx}
|
||||
className={`btn border px-12 rounded-md py-8 text-body ${idx === selectedOption && "border-primary-500 bg-primary-100 hover:bg-primary-100 text-primary-600"}`}
|
||||
onClick={() => onSelectOption(idx)}
|
||||
>
|
||||
{option.text}<AiFillThunderbolt className='inline-block text-thunder' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-body6 mt-12 text-gray-500">1 sat = 1 vote</p>
|
||||
<p className="text-body6 mt-12 text-gray-500"><strong>Where do these sats go?</strong> <br /> Claimed project votes go directly towards the maker's. Unclaimed project votes go towards BOLT.FUN's community pool.</p>
|
||||
{paymentStatus === PaymentStatus.FETCHING_PAYMENT_DETAILS && <p className="text-body6 mt-12 text-yellow-500">Please wait while we the fetch payment details.</p>}
|
||||
{paymentStatus === PaymentStatus.NOT_PAID && <p className="text-body6 mt-12 text-red-500">You did not confirm the payment. Please try again.</p>}
|
||||
{paymentStatus === PaymentStatus.PAID && <p className="text-body6 mt-12 text-green-500">The invoice was paid! Please wait while we confirm it.</p>}
|
||||
{paymentStatus === PaymentStatus.AWAITING_PAYMENT && <p className="text-body6 mt-12 text-yellow-500">Waiting for your payment...</p>}
|
||||
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <p className="text-body6 mt-12 text-green-500">Thanks for your vote</p>}
|
||||
<button
|
||||
type='submit'
|
||||
className="btn btn-primary w-full mt-32"
|
||||
disabled={paymentStatus !== PaymentStatus.DEFAULT && paymentStatus !== PaymentStatus.NOT_PAID}
|
||||
>
|
||||
{paymentStatus === PaymentStatus.DEFAULT || paymentStatus === PaymentStatus.NOT_PAID ? "Vote" : "Voting..."}
|
||||
</button>
|
||||
</form>
|
||||
{paymentStatus === PaymentStatus.PAYMENT_CONFIRMED && <Confetti width={width} height={height} />}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: VoteCard } = lazyModal(() => import('./VoteCard'))
|
||||
|
||||
|
||||
|
||||
|
||||
30
src/features/Projects/pages/ProjectPage/styles.module.scss
Normal file
30
src/features/Projects/pages/ProjectPage/styles.module.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
gap: 24px;
|
||||
grid-template-areas: "main";
|
||||
|
||||
> aside:first-of-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
grid-area: aside1;
|
||||
}
|
||||
> main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
grid-area: main;
|
||||
}
|
||||
> aside:last-of-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
grid-area: aside2;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
grid-template-areas: "aside1 main aside2";
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,5 @@ export interface ProjectCategory {
|
||||
}
|
||||
|
||||
|
||||
export type ProjectCard = Pick<Projects, 'id' | 'project' | 'logo' | 'category'>
|
||||
export type ProjectCard = Pick<Projects, 'id' | 'title' | 'logo' | 'category'>
|
||||
|
||||
|
||||
@@ -158,12 +158,12 @@ export type QueryProjectsArgs = {
|
||||
linkedIn: InputMaybe<Scalars['String']>;
|
||||
logo: InputMaybe<Array<InputMaybe<Scalars['JSON']>>>;
|
||||
openSource: InputMaybe<Scalars['String']>;
|
||||
project: InputMaybe<Scalars['String']>;
|
||||
repository: InputMaybe<Scalars['String']>;
|
||||
stars: InputMaybe<Scalars['Float']>;
|
||||
status: InputMaybe<Scalars['String']>;
|
||||
subcategory: InputMaybe<Array<InputMaybe<Scalars['String']>>>;
|
||||
telegram: InputMaybe<Scalars['String']>;
|
||||
title: InputMaybe<Scalars['String']>;
|
||||
twitter: InputMaybe<Scalars['String']>;
|
||||
updatedAt: InputMaybe<Scalars['String']>;
|
||||
watchers: InputMaybe<Scalars['Float']>;
|
||||
@@ -300,12 +300,12 @@ export type Projects = {
|
||||
linkedIn: Maybe<Scalars['String']>;
|
||||
logo: Maybe<Array<Maybe<Scalars['JSON']>>>;
|
||||
openSource: Maybe<Scalars['String']>;
|
||||
project: Maybe<Scalars['String']>;
|
||||
repository: Maybe<Scalars['String']>;
|
||||
stars: Maybe<Scalars['Float']>;
|
||||
status: Maybe<Scalars['String']>;
|
||||
subcategory: Maybe<Array<Maybe<Scalars['String']>>>;
|
||||
telegram: Maybe<Scalars['String']>;
|
||||
title: Maybe<Scalars['String']>;
|
||||
twitter: Maybe<Scalars['String']>;
|
||||
updatedAt: Maybe<Scalars['String']>;
|
||||
watchers: Maybe<Scalars['Float']>;
|
||||
@@ -327,14 +327,10 @@ export type Tags = {
|
||||
projectProductFromFeatured: Maybe<Array<Maybe<Scalars['String']>>>;
|
||||
};
|
||||
|
||||
export type SearchProjectsQueryVariables = Exact<{
|
||||
category: InputMaybe<Scalars['String']>;
|
||||
pageSize: InputMaybe<Scalars['JSON']>;
|
||||
page: InputMaybe<Scalars['JSON']>;
|
||||
}>;
|
||||
export type AllCategoriesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type SearchProjectsQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'projects', id: string | null, project: string | null, category: string | null, subcategory: Array<string | null> | null, logo: Array<any | null> | null, yearFounded: number | null } | null> | null };
|
||||
export type AllCategoriesQuery = { __typename?: 'Query', categoryList: Array<{ __typename?: 'categoryList', id: string | null, name: string | null, icon: string | null } | null> | null };
|
||||
|
||||
export type ExplorePageQueryVariables = Exact<{
|
||||
page: InputMaybe<Scalars['JSON']>;
|
||||
@@ -342,56 +338,57 @@ export type ExplorePageQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorePageQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'projects', id: string | null, project: string | null, category: string | null, logo: Array<any | null> | null, yearFounded: number | null, websiteFunctionalLightningRelated: string | null, companyName: string | null, website: string | null, description: string | null, repository: string | null, status: string | null, dead: boolean | null, twitter: string | null, linkedIn: string | null, telegram: string | null, language: string | null, updatedAt: string | null, createdAt: string | null, discord: string | null, stars: number | null } | null> | null };
|
||||
export type ExplorePageQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'projects', id: string | null, title: string | null, category: string | null, logo: Array<any | null> | null, yearFounded: number | null, websiteFunctionalLightningRelated: string | null, companyName: string | null, website: string | null, description: string | null, repository: string | null, status: string | null, dead: boolean | null, twitter: string | null, linkedIn: string | null, telegram: string | null, language: string | null, updatedAt: string | null, createdAt: string | null, discord: string | null, stars: number | null } | null> | null };
|
||||
|
||||
export type ProjectDetailsQueryVariables = Exact<{
|
||||
projectsId: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export const SearchProjectsDocument = gql`
|
||||
query SearchProjects($category: String, $pageSize: JSON, $page: JSON) {
|
||||
projects(category: $category, _page_size: $pageSize, _page: $page) {
|
||||
export type ProjectDetailsQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'projects', id: string | null, title: string | null, dead: boolean | null, createdAt: string | null, companyName: string | null, category: string | null, description: string | null, discord: string | null, endDate: string | null, twitter: string | null, updatedAt: string | null, watchers: number | null, website: string | null, yearFounded: number | null, telegram: string | null, subcategory: Array<string | null> | null, stars: number | null, repository: string | null, openSource: string | null, logo: Array<any | null> | null, linkedIn: string | null, license: string | null, language: string | null, forks: number | null, categoryList: Array<{ __typename?: 'categoryList', name: string | null } | null> | null } | null> | null };
|
||||
|
||||
|
||||
export const AllCategoriesDocument = gql`
|
||||
query AllCategories {
|
||||
categoryList {
|
||||
id
|
||||
project
|
||||
category
|
||||
subcategory
|
||||
logo
|
||||
yearFounded
|
||||
name
|
||||
icon
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useSearchProjectsQuery__
|
||||
* __useAllCategoriesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useSearchProjectsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useSearchProjectsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* To run a query within a React component, call `useAllCategoriesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAllCategoriesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useSearchProjectsQuery({
|
||||
* const { data, loading, error } = useAllCategoriesQuery({
|
||||
* variables: {
|
||||
* category: // value for 'category'
|
||||
* pageSize: // value for 'pageSize'
|
||||
* page: // value for 'page'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSearchProjectsQuery(baseOptions?: Apollo.QueryHookOptions<SearchProjectsQuery, SearchProjectsQueryVariables>) {
|
||||
export function useAllCategoriesQuery(baseOptions?: Apollo.QueryHookOptions<AllCategoriesQuery, AllCategoriesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<SearchProjectsQuery, SearchProjectsQueryVariables>(SearchProjectsDocument, options);
|
||||
return Apollo.useQuery<AllCategoriesQuery, AllCategoriesQueryVariables>(AllCategoriesDocument, options);
|
||||
}
|
||||
export function useSearchProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchProjectsQuery, SearchProjectsQueryVariables>) {
|
||||
export function useAllCategoriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllCategoriesQuery, AllCategoriesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<SearchProjectsQuery, SearchProjectsQueryVariables>(SearchProjectsDocument, options);
|
||||
return Apollo.useLazyQuery<AllCategoriesQuery, AllCategoriesQueryVariables>(AllCategoriesDocument, options);
|
||||
}
|
||||
export type SearchProjectsQueryHookResult = ReturnType<typeof useSearchProjectsQuery>;
|
||||
export type SearchProjectsLazyQueryHookResult = ReturnType<typeof useSearchProjectsLazyQuery>;
|
||||
export type SearchProjectsQueryResult = Apollo.QueryResult<SearchProjectsQuery, SearchProjectsQueryVariables>;
|
||||
export type AllCategoriesQueryHookResult = ReturnType<typeof useAllCategoriesQuery>;
|
||||
export type AllCategoriesLazyQueryHookResult = ReturnType<typeof useAllCategoriesLazyQuery>;
|
||||
export type AllCategoriesQueryResult = Apollo.QueryResult<AllCategoriesQuery, AllCategoriesQueryVariables>;
|
||||
export const ExplorePageDocument = gql`
|
||||
query ExplorePage($page: JSON, $pageSize: JSON) {
|
||||
projects(_page: $page, _page_size: $pageSize) {
|
||||
id
|
||||
project
|
||||
title
|
||||
category
|
||||
logo
|
||||
yearFounded
|
||||
@@ -441,4 +438,65 @@ export function useExplorePageLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
|
||||
}
|
||||
export type ExplorePageQueryHookResult = ReturnType<typeof useExplorePageQuery>;
|
||||
export type ExplorePageLazyQueryHookResult = ReturnType<typeof useExplorePageLazyQuery>;
|
||||
export type ExplorePageQueryResult = Apollo.QueryResult<ExplorePageQuery, ExplorePageQueryVariables>;
|
||||
export type ExplorePageQueryResult = Apollo.QueryResult<ExplorePageQuery, ExplorePageQueryVariables>;
|
||||
export const ProjectDetailsDocument = gql`
|
||||
query ProjectDetails($projectsId: String) {
|
||||
projects(id: $projectsId) {
|
||||
id
|
||||
title
|
||||
dead
|
||||
createdAt
|
||||
companyName
|
||||
category
|
||||
categoryList {
|
||||
name
|
||||
}
|
||||
description
|
||||
discord
|
||||
endDate
|
||||
twitter
|
||||
updatedAt
|
||||
watchers
|
||||
website
|
||||
yearFounded
|
||||
telegram
|
||||
subcategory
|
||||
stars
|
||||
repository
|
||||
openSource
|
||||
logo
|
||||
linkedIn
|
||||
license
|
||||
language
|
||||
forks
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useProjectDetailsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useProjectDetailsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useProjectDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useProjectDetailsQuery({
|
||||
* variables: {
|
||||
* projectsId: // value for 'projectsId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useProjectDetailsQuery(baseOptions?: Apollo.QueryHookOptions<ProjectDetailsQuery, ProjectDetailsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ProjectDetailsQuery, ProjectDetailsQueryVariables>(ProjectDetailsDocument, options);
|
||||
}
|
||||
export function useProjectDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProjectDetailsQuery, ProjectDetailsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ProjectDetailsQuery, ProjectDetailsQueryVariables>(ProjectDetailsDocument, options);
|
||||
}
|
||||
export type ProjectDetailsQueryHookResult = ReturnType<typeof useProjectDetailsQuery>;
|
||||
export type ProjectDetailsLazyQueryHookResult = ReturnType<typeof useProjectDetailsLazyQuery>;
|
||||
export type ProjectDetailsQueryResult = Apollo.QueryResult<ProjectDetailsQuery, ProjectDetailsQueryVariables>;
|
||||
@@ -8,10 +8,6 @@ let apiClientUri = "https://api.baseql.com/airtable/graphql/app7wOLbDNm617R18";
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: apiClientUri,
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Authorization': 'Bearer NWU1YTNhNGItZWQ1ZC00NWQyLTk4M2ItNWRhZGViMGYxMjQ4'
|
||||
},
|
||||
});
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError, response }) => {
|
||||
|
||||
Reference in New Issue
Block a user