feat: add similar projects component&api, fix hashtag in edit project

This commit is contained in:
MTG2000
2022-09-29 13:08:53 +03:00
parent bfc35cb6e5
commit 0c50c58ba2
12 changed files with 181 additions and 10 deletions

View File

@@ -633,6 +633,7 @@ export interface NexusGenFieldTypes {
searchProjects: NexusGenRootTypes['Project'][]; // [Project!]!
searchUsers: NexusGenRootTypes['User'][]; // [User!]!
similarMakers: NexusGenRootTypes['User'][]; // [User!]!
similarProjects: NexusGenRootTypes['Project'][]; // [Project!]!
tournamentParticipationInfo: NexusGenRootTypes['ParticipationInfo'] | null; // ParticipationInfo
}
Question: { // field return type
@@ -1021,6 +1022,7 @@ export interface NexusGenFieldTypeNames {
searchProjects: 'Project'
searchUsers: 'User'
similarMakers: 'User'
similarProjects: 'Project'
tournamentParticipationInfo: 'ParticipationInfo'
}
Question: { // field return type name
@@ -1326,6 +1328,9 @@ export interface NexusGenArgTypes {
similarMakers: { // args
id: number; // Int!
}
similarProjects: { // args
id: number; // Int!
}
tournamentParticipationInfo: { // args
tournamentId: number; // Int!
}

View File

@@ -357,6 +357,7 @@ type Query {
searchProjects(search: String!, skip: Int = 0, take: Int = 50): [Project!]!
searchUsers(value: String!): [User!]!
similarMakers(id: Int!): [User!]!
similarProjects(id: Int!): [Project!]!
tournamentParticipationInfo(tournamentId: Int!): ParticipationInfo
}

View File

@@ -441,6 +441,35 @@ const getLnurlDetailsForProject = extendType({
}
})
const similarProjects = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('similarProjects', {
type: "Project",
args: {
id: nonNull(intArg())
},
async resolve(parent, { id }, ctx) {
const currentProject = await prisma.project.findUnique({ where: { id }, select: { category_id: true } })
return prisma.project.findMany({
where: {
AND: {
id: {
not: id
},
category_id: {
equals: currentProject.category_id
}
}
},
take: 3,
})
}
})
}
})
const TeamMemberInput = inputObjectType({
name: 'TeamMemberInput',
definition(t) {
@@ -1083,6 +1112,7 @@ module.exports = {
getLnurlDetailsForProject,
getAllCapabilities,
checkValidProjectHashtag,
similarProjects,
// Mutations
createProject,

View File

@@ -14,6 +14,7 @@ import UpdateProjectContextProvider from './updateProjectContext'
import { useNavigate } from 'react-router-dom'
import { createRoute } from 'src/utils/routing'
import { nanoid } from "@reduxjs/toolkit";
import { Helmet } from "react-helmet";
interface Props {
@@ -43,9 +44,8 @@ const schema: yup.SchemaOf<IListProjectForm> = yup.object({
hashtag: yup
.string()
.required("please provide a project tag")
.transform(v => v ? '#' + v : undefined)
.matches(
/^#[^ !@#$%^&*(),.?":{}|<>]*$/,
/^[^ !@#$%^&*(),.?":{}|<>]*$/,
"your project's tag can only contain letters, numbers and '_"
)
.min(3, "your project tag must be longer than 2 characters.")
@@ -161,7 +161,7 @@ export default function FormContainer(props: PropsWithChildren<Props>) {
tagline: data.tagline,
website: data.website,
description: data.description,
hashtag: data.hashtag.slice(1),
hashtag: data.hashtag,
twitter: data.twitter,
discord: data.discord,
slack: data.slack,
@@ -191,7 +191,10 @@ export default function FormContainer(props: PropsWithChildren<Props>) {
if (query.loading)
return <LoadingPage />
return (
return (<>
<Helmet>
<title>{isUpdating ? "Update project" : "List a project"}</title>
</Helmet>
<FormProvider {...methods} >
<UpdateProjectContextProvider permissions={query.data?.getProject.permissions ?? Object.values(ProjectPermissionEnum)}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
@@ -199,7 +202,7 @@ export default function FormContainer(props: PropsWithChildren<Props>) {
</form>
</UpdateProjectContextProvider>
</FormProvider>
)
</>)
}

View File

@@ -9,6 +9,9 @@ import { NotificationsService } from 'src/services'
import { useAppDispatch } from 'src/utils/hooks';
import { openModal } from 'src/redux/features/modals.slice';
import { useCreateProjectMutation, useUpdateProjectMutation, UpdateProjectInput } from 'src/graphql'
import { Link } from 'react-router-dom';
import { createRoute } from 'src/utils/routing';
import { wrapLink } from 'src/utils/hoc';
interface Props {
currentTab: keyof typeof tabs
@@ -29,7 +32,7 @@ export default function SaveChangesCard(props: Props) {
const isLoading = updatingStatus.loading || creatingStatus.loading
const [img, name, tagline] = watch(['thumbnail_image', 'title', 'tagline',])
const [hashtag, img, name, tagline] = watch(['hashtag', 'thumbnail_image', 'title', 'tagline',])
const clickCancel = () => {
if (window.confirm('You might lose some unsaved changes. Are you sure you want to continue?'))
@@ -115,7 +118,7 @@ export default function SaveChangesCard(props: Props) {
return (
<Card className='flex flex-col gap-24'>
<div className='flex gap-8 items-center'>
{wrapLink(<div className='flex gap-8 items-center'>
{img ?
<Avatar width={48} src={img.url} /> :
<div className="bg-gray-50 border border-gray-200 rounded-full w-48 h-48 shrink-0"></div>
@@ -124,7 +127,8 @@ export default function SaveChangesCard(props: Props) {
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{name || "Product preview"}</p>
{<p className={`text-body6 text-gray-600 text-ellipsis overflow-hidden whitespace-nowrap`}>{tagline || "Provide some more details."}</p>}
</div>
</div>
</div>, isUpdating ? createRoute({ type: "project", tag: hashtag }) : undefined)}
<div className="border-b border-gray-200"></div>
{/* <p className="hidden md:block text-body5">{trimText(profileQuery.data.profile.bio, 120)}</p> */}
<div className="flex flex-col gap-16">

View File

@@ -16,6 +16,7 @@ export default function MakersCard({ members }: Props) {
<p className="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>}
{members.map(m => <Link key={m.user.id} to={createRoute({ type: "profile", id: m.user.id, username: m.user.name })}>
<Avatar
width={40}

View File

@@ -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 = {
}

View File

@@ -0,0 +1,36 @@
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;
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="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 font-medium">{project.category.icon} {project.category.title}</p>
</div>
</li>
</Link>
})}
</ul>
</Card>
)
}

View File

@@ -56,3 +56,17 @@ query ProjectDetails($projectId: Int, $projectTag: String) {
}
}
}
query SimilarProjects($projectId: Int!) {
similarProjects(id: $projectId) {
id
title
hashtag
thumbnail_image
category {
id
icon
title
}
}
}

View File

@@ -74,8 +74,7 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop
}
</div>
</div>
<hr className="my-40" />
<div className="text-center h-[100px]">
<div className="text-center h-[40px]">
</div>
</div>

View File

@@ -14,6 +14,7 @@ import TournamentsCard from "src/features/Profiles/pages/ProfilePage/Tournaments
import StoriesCard from "src/features/Profiles/pages/ProfilePage/StoriesCard/StoriesCard"
import MakersCard from "./Components/MakersCard/MakersCard"
import AboutCard from "./Components/AboutCard/AboutCard"
import SimilarProjectsCard from "./Components/SimilarProjectsCard/SimilarProjectsCard"
export default function ProjectPage() {
@@ -77,6 +78,7 @@ export default function ProjectPage() {
</main>
<aside className="min-w-0">
<MakersCard members={project.members} />
<SimilarProjectsCard id={project.id} />
</aside>
</>
:

View File

@@ -476,6 +476,7 @@ export type Query = {
searchProjects: Array<Project>;
searchUsers: Array<User>;
similarMakers: Array<User>;
similarProjects: Array<Project>;
tournamentParticipationInfo: Maybe<ParticipationInfo>;
};
@@ -598,6 +599,11 @@ export type QuerySimilarMakersArgs = {
};
export type QuerySimilarProjectsArgs = {
id: Scalars['Int'];
};
export type QueryTournamentParticipationInfoArgs = {
tournamentId: Scalars['Int'];
};
@@ -1080,6 +1086,13 @@ export type ProjectDetailsQueryVariables = Exact<{
export type ProjectDetailsQuery = { __typename?: 'Query', getProject: { __typename?: 'Project', id: number, title: string, tagline: string, description: string, hashtag: string, cover_image: string, thumbnail_image: string, launch_status: ProjectLaunchStatusEnum, twitter: string | null, discord: string | null, github: string | null, slack: string | null, telegram: string | null, screenshots: Array<string>, website: string, lightning_address: string | null, votes_count: number, permissions: Array<ProjectPermissionEnum>, category: { __typename?: 'Category', id: number, icon: string | null, title: string }, members: Array<{ __typename?: 'ProjectMember', role: Team_Member_Role, user: { __typename?: 'User', id: number, name: string, jobTitle: string | null, avatar: string } }>, awards: Array<{ __typename?: 'Award', title: string, image: string, url: string, id: number }>, tags: Array<{ __typename?: 'Tag', id: number, title: string }>, recruit_roles: Array<{ __typename?: 'MakerRole', id: number, title: string, icon: string, level: RoleLevelEnum }>, capabilities: Array<{ __typename?: 'Capability', id: number, title: string, icon: string }> } };
export type SimilarProjectsQueryVariables = Exact<{
projectId: Scalars['Int'];
}>;
export type SimilarProjectsQuery = { __typename?: 'Query', similarProjects: Array<{ __typename?: 'Project', id: number, title: string, hashtag: string, thumbnail_image: string, category: { __typename?: 'Category', id: number, icon: string | null, title: string } }> };
export type GetAllRolesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2763,6 +2776,49 @@ export function useProjectDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type ProjectDetailsQueryHookResult = ReturnType<typeof useProjectDetailsQuery>;
export type ProjectDetailsLazyQueryHookResult = ReturnType<typeof useProjectDetailsLazyQuery>;
export type ProjectDetailsQueryResult = Apollo.QueryResult<ProjectDetailsQuery, ProjectDetailsQueryVariables>;
export const SimilarProjectsDocument = gql`
query SimilarProjects($projectId: Int!) {
similarProjects(id: $projectId) {
id
title
hashtag
thumbnail_image
category {
id
icon
title
}
}
}
`;
/**
* __useSimilarProjectsQuery__
*
* To run a query within a React component, call `useSimilarProjectsQuery` and pass it any options that fit your needs.
* When your component renders, `useSimilarProjectsQuery` 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 } = useSimilarProjectsQuery({
* variables: {
* projectId: // value for 'projectId'
* },
* });
*/
export function useSimilarProjectsQuery(baseOptions: Apollo.QueryHookOptions<SimilarProjectsQuery, SimilarProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SimilarProjectsQuery, SimilarProjectsQueryVariables>(SimilarProjectsDocument, options);
}
export function useSimilarProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SimilarProjectsQuery, SimilarProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SimilarProjectsQuery, SimilarProjectsQueryVariables>(SimilarProjectsDocument, options);
}
export type SimilarProjectsQueryHookResult = ReturnType<typeof useSimilarProjectsQuery>;
export type SimilarProjectsLazyQueryHookResult = ReturnType<typeof useSimilarProjectsLazyQuery>;
export type SimilarProjectsQueryResult = Apollo.QueryResult<SimilarProjectsQuery, SimilarProjectsQueryVariables>;
export const GetAllRolesDocument = gql`
query GetAllRoles {
getAllMakersRoles {