diff --git a/api/functions/graphql/nexus-typegen.ts b/api/functions/graphql/nexus-typegen.ts index e40320a..8310857 100644 --- a/api/functions/graphql/nexus-typegen.ts +++ b/api/functions/graphql/nexus-typegen.ts @@ -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! } diff --git a/api/functions/graphql/schema.graphql b/api/functions/graphql/schema.graphql index 2957a37..6f64e81 100644 --- a/api/functions/graphql/schema.graphql +++ b/api/functions/graphql/schema.graphql @@ -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 } diff --git a/api/functions/graphql/types/project.js b/api/functions/graphql/types/project.js index 0c6cc1a..36e1d18 100644 --- a/api/functions/graphql/types/project.js +++ b/api/functions/graphql/types/project.js @@ -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, diff --git a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx index d10d3af..37fd9e0 100644 --- a/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx +++ b/src/features/Projects/pages/ListProjectPage/Components/FormContainer/FormContainer.tsx @@ -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 = 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) { 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) { if (query.loading) return - return ( + return (<> + + {isUpdating ? "Update project" : "List a project"} +
@@ -199,7 +202,7 @@ export default function FormContainer(props: PropsWithChildren) {
- ) + ) } diff --git a/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx b/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx index c3b6256..08fa676 100644 --- a/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx +++ b/src/features/Projects/pages/ListProjectPage/Components/SaveChangesCard/SaveChangesCard.tsx @@ -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 ( -
+ {wrapLink(
{img ? :
@@ -124,7 +127,8 @@ export default function SaveChangesCard(props: Props) {

{name || "Product preview"}

{

{tagline || "Provide some more details."}

}
-
+ , isUpdating ? createRoute({ type: "project", tag: hashtag }) : undefined)} +
{/*

{trimText(profileQuery.data.profile.bio, 120)}

*/}
diff --git a/src/features/Projects/pages/ProjectPage/Components/MakersCard/MakersCard.tsx b/src/features/Projects/pages/ProjectPage/Components/MakersCard/MakersCard.tsx index efb3050..cd16c35 100644 --- a/src/features/Projects/pages/ProjectPage/Components/MakersCard/MakersCard.tsx +++ b/src/features/Projects/pages/ProjectPage/Components/MakersCard/MakersCard.tsx @@ -16,6 +16,7 @@ export default function MakersCard({ members }: Props) {

👾 Makers

+ {members.length === 0 &&

Not listed

} {members.map(m => ; + + +const Template: ComponentStory = (args) =>
+ +export const Default = Template.bind({}); +Default.args = { +} + + diff --git a/src/features/Projects/pages/ProjectPage/Components/SimilarProjectsCard/SimilarProjectsCard.tsx b/src/features/Projects/pages/ProjectPage/Components/SimilarProjectsCard/SimilarProjectsCard.tsx new file mode 100644 index 0000000..18dde11 --- /dev/null +++ b/src/features/Projects/pages/ProjectPage/Components/SimilarProjectsCard/SimilarProjectsCard.tsx @@ -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 ( + +

🚀 Similar projects

+
    + {query.data?.similarProjects.map(project => { + return +
  • + +
    +

    {project.title}

    +

    {project.category.icon} {project.category.title}

    +
    +
  • + + })} +
+
+ ) +} diff --git a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql index cbca6ac..d867b65 100644 --- a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql +++ b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetails.graphql @@ -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 + } + } +} diff --git a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx index 2501813..ac5edf5 100644 --- a/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx +++ b/src/features/Projects/pages/ProjectPage/ProjectDetailsCard/ProjectDetailsCard.Skeleton.tsx @@ -74,8 +74,7 @@ export default function ProjectDetailsCardSkeleton({ onClose, direction, ...prop }
-
-
+
diff --git a/src/features/Projects/pages/ProjectPage/ProjectPage.tsx b/src/features/Projects/pages/ProjectPage/ProjectPage.tsx index 81ba927..2fc5870 100644 --- a/src/features/Projects/pages/ProjectPage/ProjectPage.tsx +++ b/src/features/Projects/pages/ProjectPage/ProjectPage.tsx @@ -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() { : diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index 0b9875a..29d3326 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -476,6 +476,7 @@ export type Query = { searchProjects: Array; searchUsers: Array; similarMakers: Array; + similarProjects: Array; tournamentParticipationInfo: Maybe; }; @@ -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, website: string, lightning_address: string | null, votes_count: number, permissions: Array, 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; export type ProjectDetailsLazyQueryHookResult = ReturnType; export type ProjectDetailsQueryResult = Apollo.QueryResult; +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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(SimilarProjectsDocument, options); + } +export function useSimilarProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(SimilarProjectsDocument, options); + } +export type SimilarProjectsQueryHookResult = ReturnType; +export type SimilarProjectsLazyQueryHookResult = ReturnType; +export type SimilarProjectsQueryResult = Apollo.QueryResult; export const GetAllRolesDocument = gql` query GetAllRoles { getAllMakersRoles {