mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-16 20:14:19 +01:00
feat: add "hashtag" field to api, add images validation to FE, make launch_status an enum field
This commit is contained in:
@@ -35,8 +35,9 @@ export interface NexusGenInputs {
|
||||
description: string; // String!
|
||||
discord?: string | null; // String
|
||||
github?: string | null; // String
|
||||
hashtag: string; // String!
|
||||
id?: number | null; // Int
|
||||
launch_status: string; // String!
|
||||
launch_status: NexusGenEnums['ProjectLaunchStatusEnum']; // ProjectLaunchStatusEnum!
|
||||
members: NexusGenInputs['TeamMemberInput'][]; // [TeamMemberInput!]!
|
||||
recruit_roles: number[]; // [Int!]!
|
||||
screenshots: NexusGenInputs['ImageInput'][]; // [ImageInput!]!
|
||||
@@ -100,8 +101,9 @@ export interface NexusGenInputs {
|
||||
description: string; // String!
|
||||
discord?: string | null; // String
|
||||
github?: string | null; // String
|
||||
hashtag: string; // String!
|
||||
id?: number | null; // Int
|
||||
launch_status: string; // String!
|
||||
launch_status: NexusGenEnums['ProjectLaunchStatusEnum']; // ProjectLaunchStatusEnum!
|
||||
members: NexusGenInputs['TeamMemberInput'][]; // [TeamMemberInput!]!
|
||||
recruit_roles: number[]; // [Int!]!
|
||||
screenshots: NexusGenInputs['ImageInput'][]; // [ImageInput!]!
|
||||
@@ -124,6 +126,7 @@ export interface NexusGenInputs {
|
||||
|
||||
export interface NexusGenEnums {
|
||||
POST_TYPE: "Bounty" | "Question" | "Story"
|
||||
ProjectLaunchStatusEnum: "Launched" | "WIP"
|
||||
RoleLevelEnum: 3 | 0 | 1 | 2 | 4
|
||||
TEAM_MEMBER_ROLE: "Admin" | "Maker"
|
||||
TournamentEventTypeEnum: 2 | 3 | 0 | 1
|
||||
|
||||
@@ -84,8 +84,9 @@ input CreateProjectInput {
|
||||
description: String!
|
||||
discord: String
|
||||
github: String
|
||||
hashtag: String!
|
||||
id: Int
|
||||
launch_status: String!
|
||||
launch_status: ProjectLaunchStatusEnum!
|
||||
members: [TeamMemberInput!]!
|
||||
recruit_roles: [Int!]!
|
||||
screenshots: [ImageInput!]!
|
||||
@@ -288,6 +289,11 @@ type Project {
|
||||
website: String!
|
||||
}
|
||||
|
||||
enum ProjectLaunchStatusEnum {
|
||||
Launched
|
||||
WIP
|
||||
}
|
||||
|
||||
type Query {
|
||||
allCategories: [Category!]!
|
||||
allProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
@@ -475,8 +481,9 @@ input UpdateProjectInput {
|
||||
description: String!
|
||||
discord: String
|
||||
github: String
|
||||
hashtag: String!
|
||||
id: Int
|
||||
launch_status: String!
|
||||
launch_status: ProjectLaunchStatusEnum!
|
||||
members: [TeamMemberInput!]!
|
||||
recruit_roles: [Int!]!
|
||||
screenshots: [ImageInput!]!
|
||||
|
||||
@@ -303,11 +303,17 @@ const TeamMemberInput = inputObjectType({
|
||||
}
|
||||
})
|
||||
|
||||
const ProjectLaunchStatusEnum = enumType({
|
||||
name: 'ProjectLaunchStatusEnum',
|
||||
members: ['WIP', 'Launched'],
|
||||
});
|
||||
|
||||
const CreateProjectInput = inputObjectType({
|
||||
name: 'CreateProjectInput',
|
||||
definition(t) {
|
||||
t.int('id') // exists in update
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('hashtag');
|
||||
t.nonNull.string('website');
|
||||
t.nonNull.string('tagline');
|
||||
t.nonNull.string('description');
|
||||
@@ -329,7 +335,9 @@ const CreateProjectInput = inputObjectType({
|
||||
type: TeamMemberInput
|
||||
});
|
||||
t.nonNull.list.nonNull.int('recruit_roles'); // ids
|
||||
t.nonNull.string('launch_status'); // "wip" | "launched"
|
||||
t.nonNull.field('launch_status', {
|
||||
type: ProjectLaunchStatusEnum
|
||||
});
|
||||
t.nonNull.list.nonNull.int('tournaments'); // ids
|
||||
}
|
||||
})
|
||||
@@ -361,6 +369,7 @@ const UpdateProjectInput = inputObjectType({
|
||||
definition(t) {
|
||||
t.int('id')
|
||||
t.nonNull.string('title');
|
||||
t.nonNull.string('hashtag');
|
||||
t.nonNull.string('website');
|
||||
t.nonNull.string('tagline');
|
||||
t.nonNull.string('description');
|
||||
@@ -382,7 +391,9 @@ const UpdateProjectInput = inputObjectType({
|
||||
type: TeamMemberInput
|
||||
});
|
||||
t.nonNull.list.nonNull.int('recruit_roles'); // ids
|
||||
t.nonNull.string('launch_status'); // "wip" | "launched"
|
||||
t.nonNull.field('launch_status', {
|
||||
type: ProjectLaunchStatusEnum
|
||||
});
|
||||
t.nonNull.list.nonNull.int('tournaments'); // ids
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,18 +2,16 @@ import { Controller, useFormContext } from "react-hook-form"
|
||||
import Card from "src/Components/Card/Card";
|
||||
import TournamentsInput from "../TournamentsInput/TournamentsInput";
|
||||
import { IListProjectForm } from "../FormContainer/FormContainer";
|
||||
import { ProjectLaunchStatusEnum } from "src/graphql";
|
||||
|
||||
interface Props { }
|
||||
|
||||
export default function ExtrasTab(props: Props) {
|
||||
|
||||
const { register, formState: { errors, isDirty, }, control } = useFormContext<IListProjectForm>();
|
||||
const { register, formState: { errors, }, control } = useFormContext<IListProjectForm>();
|
||||
|
||||
|
||||
|
||||
// usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-24">
|
||||
<Card>
|
||||
@@ -25,7 +23,7 @@ export default function ExtrasTab(props: Props) {
|
||||
{...register("launch_status")}
|
||||
type="radio"
|
||||
name="launch_status"
|
||||
value='wip'
|
||||
value={ProjectLaunchStatusEnum.Wip}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-body4 font-medium">WIP 🛠️</p>
|
||||
@@ -37,7 +35,7 @@ export default function ExtrasTab(props: Props) {
|
||||
{...register("launch_status")}
|
||||
type="radio"
|
||||
name="launch_status"
|
||||
value='launched'
|
||||
value={ProjectLaunchStatusEnum.Launched}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-body4 font-medium">Launched 🚀</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Team_Member_Role, UpdateProjectInput } from "src/graphql";
|
||||
import { ProjectLaunchStatusEnum, Team_Member_Role, UpdateProjectInput } from "src/graphql";
|
||||
import { PropsWithChildren, useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { usePrompt } from "src/utils/hooks";
|
||||
@@ -13,38 +13,6 @@ interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// export interface IListProjectForm {
|
||||
// id?: number
|
||||
// title: string
|
||||
// website: string
|
||||
// tagline: string
|
||||
// description: string
|
||||
// thumbnail_image?: string
|
||||
// cover_image?: string
|
||||
// twitter?: string
|
||||
// discord?: string
|
||||
// github?: string
|
||||
// category_id: number
|
||||
// capabilities: NestedValue<string[]>
|
||||
// screenshots: NestedValue<string[]>
|
||||
|
||||
// members: NestedValue<{
|
||||
// id: number,
|
||||
// name: string,
|
||||
// jobTitle: string | null,
|
||||
// avatar: string,
|
||||
// role: Team_Member_Role,
|
||||
// }[]>
|
||||
// recruit_roles: NestedValue<string[]>
|
||||
|
||||
|
||||
// launch_status: "wip" | "launched"
|
||||
// tournaments: NestedValue<string[]>
|
||||
// }
|
||||
|
||||
|
||||
|
||||
export type IListProjectForm = Override<UpdateProjectInput, {
|
||||
members: NestedValue<{
|
||||
id: number,
|
||||
@@ -56,25 +24,34 @@ export type IListProjectForm = Override<UpdateProjectInput, {
|
||||
capabilities: NestedValue<UpdateProjectInput['capabilities']>
|
||||
recruit_roles: NestedValue<UpdateProjectInput['recruit_roles']>
|
||||
tournaments: NestedValue<UpdateProjectInput['tournaments']>
|
||||
cover_image: NestedValue<UpdateProjectInput['cover_image']>
|
||||
thumbnail_image: NestedValue<UpdateProjectInput['thumbnail_image']>
|
||||
}>
|
||||
|
||||
const schema: yup.SchemaOf<IListProjectForm> = yup.object({
|
||||
id: yup.number().optional(),
|
||||
title: yup.string().trim().required().min(2),
|
||||
website: yup.string().trim().url().required(),
|
||||
hashtag: yup
|
||||
.string()
|
||||
.required()
|
||||
.matches(
|
||||
/^(?:#)([A-Za-z0-9_](?:(?:[A-Za-z0-9_]|(?:(?!))){0,28}(?:[A-Za-z0-9_]))?)((?: #)([A-Za-z0-9_](?:(?:[A-Za-z0-9_]|(?:\.(?!\.))){0,28}(?:[A-Za-z0-9_]))?))*$/,
|
||||
"Invalid format for hashtag"
|
||||
),
|
||||
website: yup.string().trim().url().required().label("project's link"),
|
||||
tagline: yup.string().trim().required().min(10),
|
||||
description: yup.string().trim().required().min(50, 'Write at least 10 words descriping your project'),
|
||||
thumbnail_image: imageSchema.required(),
|
||||
cover_image: imageSchema.required(),
|
||||
twitter: yup.string().url().ensure(),
|
||||
discord: yup.string().url().ensure(),
|
||||
github: yup.string().url().ensure(),
|
||||
thumbnail_image: imageSchema.required("Please pick a thumbnail image").default(undefined),
|
||||
cover_image: imageSchema.required("Please pick a cover image").default(undefined),
|
||||
twitter: yup.string().url(),
|
||||
discord: yup.string().url(),
|
||||
github: yup.string().url(),
|
||||
category_id: yup.number().required("Please choose a category"),
|
||||
capabilities: yup.array().of(yup.string().required()).default([]),
|
||||
screenshots: yup.array().of(imageSchema.required()).default([]),
|
||||
members: yup.array().of(yup.object() as any).default([]),
|
||||
recruit_roles: yup.array().of(yup.number().required()).default([]),
|
||||
launch_status: yup.mixed().oneOf(['wip', 'launched']).default('wip'),
|
||||
launch_status: yup.mixed().oneOf([ProjectLaunchStatusEnum.Wip, ProjectLaunchStatusEnum.Launched]).default(ProjectLaunchStatusEnum.Wip),
|
||||
tournaments: yup.array().of(yup.number().required()).default([])
|
||||
}).required();
|
||||
|
||||
@@ -84,6 +61,8 @@ export default function FormContainer(props: PropsWithChildren<Props>) {
|
||||
|
||||
const methods = useForm<IListProjectForm>({
|
||||
defaultValues: {
|
||||
cover_image: undefined,
|
||||
thumbnail_image: undefined,
|
||||
id: !!params.get('id') ? Number(params.get('id')) : undefined,
|
||||
title: "",
|
||||
website: "",
|
||||
@@ -97,7 +76,7 @@ export default function FormContainer(props: PropsWithChildren<Props>) {
|
||||
screenshots: [],
|
||||
members: [],
|
||||
recruit_roles: [],
|
||||
launch_status: 'wip',
|
||||
launch_status: ProjectLaunchStatusEnum.Wip,
|
||||
tournaments: [],
|
||||
},
|
||||
resolver: yupResolver(schema) as Resolver<IListProjectForm>,
|
||||
|
||||
@@ -15,11 +15,6 @@ export default function ProjectDetailsTab(props: Props) {
|
||||
|
||||
const { register, formState: { errors, }, control, getValues } = useFormContext<IListProjectForm>();
|
||||
|
||||
|
||||
// usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="md:col-span-2 flex flex-col gap-24">
|
||||
<Card className="" defaultPadding={false}>
|
||||
@@ -33,7 +28,6 @@ export default function ProjectDetailsTab(props: Props) {
|
||||
onChange={e => {
|
||||
onChange(e)
|
||||
}}
|
||||
// uploadText='Add a cover image'
|
||||
/>
|
||||
|
||||
}
|
||||
@@ -48,7 +42,16 @@ export default function ProjectDetailsTab(props: Props) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-16 md:p-24 mt-64">
|
||||
{(errors.cover_image || errors.thumbnail_image) && <div className="mb-16">
|
||||
{errors.cover_image && <p className="input-error">
|
||||
{errors.cover_image.message}
|
||||
</p>}
|
||||
{errors.thumbnail_image && <p className="input-error">
|
||||
{errors.thumbnail_image.message}
|
||||
</p>}
|
||||
</div>}
|
||||
<p className="text-body5 font-medium">
|
||||
Project name<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
@@ -110,7 +113,20 @@ export default function ProjectDetailsTab(props: Props) {
|
||||
{errors.description && <p className="input-error">
|
||||
{errors.description.message}
|
||||
</p>}
|
||||
|
||||
<p className="text-body5 font-medium mt-16">
|
||||
Hashtag<sup className="text-red-500">*</sup>
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder='#my_awesome_app'
|
||||
{...register("hashtag")}
|
||||
/>
|
||||
</div>
|
||||
{errors.hashtag && <p className="input-error">
|
||||
{errors.hashtag.message}
|
||||
</p>}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="">
|
||||
@@ -203,7 +219,7 @@ export default function ProjectDetailsTab(props: Props) {
|
||||
|
||||
<Card>
|
||||
<h2 className="text-body2 font-bolder">📷 Screenshots</h2>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Choose up to 4 images from your project</p>
|
||||
<p className="text-body4 font-light text-gray-600 mt-8">Choose up to 4 screenshots from your project</p>
|
||||
<div className="mt-24">
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
import Card from 'src/Components/Card/Card'
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { IListProjectForm } from "../FormContainer/FormContainer";
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { tabs } from '../../ListProjectPage'
|
||||
import { NotificationsService } from 'src/services'
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
@@ -19,15 +18,16 @@ interface Props {
|
||||
|
||||
export default function SaveChangesCard(props: Props) {
|
||||
|
||||
const { handleSubmit, formState: { errors, isDirty, }, reset, getValues, watch } = useFormContext<IListProjectForm>();
|
||||
const { handleSubmit, formState: { isDirty, }, reset, getValues, watch } = useFormContext<IListProjectForm>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isUpdating = useMemo(() => !!getValues('id'), [getValues]);
|
||||
|
||||
const [update, updatingStatus] = useUpdateProjectMutation();
|
||||
const [create, creatingStatus] = useCreateProjectMutation()
|
||||
|
||||
const isLoading = updatingStatus.loading || creatingStatus.loading
|
||||
|
||||
|
||||
const [img, name, tagline] = watch(['thumbnail_image', 'title', 'tagline',])
|
||||
|
||||
|
||||
@@ -109,8 +109,9 @@ export type CreateProjectInput = {
|
||||
description: Scalars['String'];
|
||||
discord?: InputMaybe<Scalars['String']>;
|
||||
github?: InputMaybe<Scalars['String']>;
|
||||
hashtag: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['Int']>;
|
||||
launch_status: Scalars['String'];
|
||||
launch_status: ProjectLaunchStatusEnum;
|
||||
members: Array<TeamMemberInput>;
|
||||
recruit_roles: Array<Scalars['Int']>;
|
||||
screenshots: Array<ImageInput>;
|
||||
@@ -404,6 +405,11 @@ export type Project = {
|
||||
website: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum ProjectLaunchStatusEnum {
|
||||
Launched = 'Launched',
|
||||
Wip = 'WIP'
|
||||
}
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
allCategories: Array<Category>;
|
||||
@@ -719,8 +725,9 @@ export type UpdateProjectInput = {
|
||||
description: Scalars['String'];
|
||||
discord?: InputMaybe<Scalars['String']>;
|
||||
github?: InputMaybe<Scalars['String']>;
|
||||
hashtag: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['Int']>;
|
||||
launch_status: Scalars['String'];
|
||||
launch_status: ProjectLaunchStatusEnum;
|
||||
members: Array<TeamMemberInput>;
|
||||
recruit_roles: Array<Scalars['Int']>;
|
||||
screenshots: Array<ImageInput>;
|
||||
|
||||
Reference in New Issue
Block a user