mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-23 15:34:21 +01:00
feat: add similar projects component&api, fix hashtag in edit project
This commit is contained in:
@@ -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!
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
</>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user