feat: added form state management

This commit is contained in:
MTG2000
2022-08-16 12:15:45 +03:00
parent 66b4278768
commit 37117cf0f7
11 changed files with 550 additions and 568 deletions

View File

@@ -5,6 +5,7 @@ import { MEDIA_QUERIES } from "src/utils/theme/media_queries";
import CustomDot from "./CustomDot/CustomDot";
import useEmblaCarousel from 'embla-carousel-react'
import { useCallback, useEffect, useState } from "react";
import { createRoute } from "src/utils/routing";
const headerLinks = [
{
@@ -12,7 +13,8 @@ const headerLinks = [
img: Assets.Images_ExploreHeader1,
link: {
content: "Submit project",
url: "https://form.jotform.com/220301236112030",
url: createRoute({ type: "edit-project" }),
newTab: false,
},
},
{
@@ -25,6 +27,7 @@ const headerLinks = [
link: {
content: "Register Now",
url: "https://bolt.fun/hackathons/shock-the-web-2/",
newTab: true,
},
},
];
@@ -72,7 +75,7 @@ export default function Header() {
{headerLinks[0].title}
</div>
<Button href={headerLinks[0].link.url} newTab color="white" className="mt-24">
<Button href={headerLinks[0].link.url} newTab={headerLinks[0].link.newTab} color="white" className="mt-24">
{headerLinks[0].link.content}
</Button>
</div>
@@ -86,7 +89,7 @@ export default function Header() {
<div className="max-w-[90%]">
{headerLinks[1].title}
</div>
<Button color="white" href={headerLinks[1].link.url} newTab className="mt-24">
<Button color="white" href={headerLinks[1].link.url} newTab={headerLinks[1].link.newTab} className="mt-24">
{headerLinks[1].link.content}
</Button>
</div>

View File

@@ -1,143 +1,70 @@
import { Controller, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"
import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { usePrompt } from "src/utils/hooks";
import { toast } from "react-toastify";
import { Controller, useFormContext } from "react-hook-form"
import Card from "src/Components/Card/Card";
import TeamMembersInput from "../TeamMembersInput/TeamMembersInput";
import { Team_Member_Role } from "src/graphql";
import RecruitRolesInput from "../RecruitRolesInput/RecruitRolesInput";
import TournamentsInput from "../TournamentsInput/TournamentsInput";
import { IListProjectForm } from "../FormContainer/FormContainer";
interface Props { }
export interface IExtrasForm {
launch_status: "wip" | "launched"
tournaments: NestedValue<string[]>
}
export default function ExtrasTab(props: Props) {
interface Props {
data?: IExtrasForm,
}
// type IFormInputs = Props['data'];
const schema: yup.SchemaOf<IExtrasForm> = yup.object({
launch_status: yup.mixed().oneOf(['wip', 'launched']).required(),
tournaments: yup.array().required().default([])
}).required();
export default function ExtrasTab({ data }: Props) {
const { register, formState: { errors, isDirty, }, handleSubmit, reset, control } = useForm<IExtrasForm>({
defaultValues: {
...data,
launch_status: 'wip',
tournaments: []
},
resolver: yupResolver(schema) as Resolver<IExtrasForm>,
// mode: 'onBlur',
});
const { register, formState: { errors, isDirty, }, control } = useFormContext<IListProjectForm>();
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
// usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
const onSubmit: SubmitHandler<IExtrasForm> = data => {
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
const mutate: any = null;
mutate({
// variables: {
// data: {
// // name: data.name,
// // avatar: data.avatar,
// // jobTitle: data.jobTitle,
// // bio: data.bio,
// // email: data.email,
// // github: data.github,
// // linkedin: data.linkedin,
// // lightning_address: data.lightning_address,
// // location: data.location,
// // twitter: data.twitter,
// // website: data.website,
// }
// },
onCompleted: () => {
reset(data);
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
}
})
.catch(() => {
toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false });
// mutationStatus.reset()
})
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="md:col-span-2 flex flex-col gap-24">
<Card>
<h2 className="text-body2 font-bolder">🚀 Launch status</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Has this product been launched already, or is it still a work in progress?</p>
<div className="mt-24 flex flex-col gap-24">
<div className="flex gap-16">
<input
{...register("launch_status")}
type="radio"
name="launch_status"
value='wip'
/>
<div>
<p className="text-body4 font-medium">WIP 🛠</p>
<p className="text-body5 text-gray-500 mt-4">Its still a Work In Progress.</p>
</div>
</div>
<div className="flex gap-16">
<input
{...register("launch_status")}
type="radio"
name="launch_status"
value='launched'
/>
<div>
<p className="text-body4 font-medium">Launched 🚀</p>
<p className="text-body5 text-gray-500 mt-4">The product is ready for launch, or has been launched already.</p>
</div>
</div>
{errors.launch_status && <p className='input-error'>{errors.launch_status?.message}</p>}
</div>
</Card>
<Card>
<h2 className="text-body2 font-bolder"> Tournaments</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Is your application part of a tournament? If so, select the tournament(s) that apply and it will automatically be listed for you.</p>
<div className="mt-24">
<Controller
control={control}
name="tournaments"
render={({ field: { onChange, value } }) => (
<TournamentsInput
value={value}
onChange={onChange}
/>
)}
<div className="flex flex-col gap-24">
<Card>
<h2 className="text-body2 font-bolder">🚀 Launch status</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Has this product been launched already, or is it still a work in progress?</p>
<div className="mt-24 flex flex-col gap-24">
<div className="flex gap-16">
<input
{...register("launch_status")}
type="radio"
name="launch_status"
value='wip'
/>
{errors.tournaments && <p className='input-error'>{errors.tournaments?.message}</p>}
<div>
<p className="text-body4 font-medium">WIP 🛠</p>
<p className="text-body5 text-gray-500 mt-4">Its still a Work In Progress.</p>
</div>
</div>
</Card>
</div>
<div className="self-start sticky-side-element">
{/* <SaveChangesCard
isLoading={mutationStatus.loading}
isDirty={isDirty}
onSubmit={handleSubmit(onSubmit)}
onCancel={() => reset()}
/> */}
</div>
<div className="flex gap-16">
<input
{...register("launch_status")}
type="radio"
name="launch_status"
value='launched'
/>
<div>
<p className="text-body4 font-medium">Launched 🚀</p>
<p className="text-body5 text-gray-500 mt-4">The product is ready for launch, or has been launched already.</p>
</div>
</div>
{errors.launch_status && <p className='input-error'>{errors.launch_status?.message}</p>}
</div>
</Card>
<Card>
<h2 className="text-body2 font-bolder"> Tournaments</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Is your application part of a tournament? If so, select the tournament(s) that apply and it will automatically be listed for you.</p>
<div className="mt-24">
<Controller
control={control}
name="tournaments"
render={({ field: { onChange, value } }) => (
<TournamentsInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.tournaments && <p className='input-error'>{errors.tournaments?.message}</p>}
</div>
</Card>
</div>
)
}

View File

@@ -1,12 +1,91 @@
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 } from "src/graphql";
import { PropsWithChildren } from "react";
import { useSearchParams } from "react-router-dom";
interface Props {
}
export default function FormContainer() {
return (
<div>
</div>
export interface IListProjectForm {
id?: number
name: 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[]>
}
const schema: yup.SchemaOf<IListProjectForm> = yup.object({
id: yup.number().optional(),
name: yup.string().trim().required().min(2),
website: yup.string().trim().url().required(),
tagline: yup.string().trim().required().min(10),
description: yup.string().trim().required().min(50, 'Write at least 10 words descriping your project'),
thumbnail_image: yup.string().url().ensure(),
cover_image: yup.string().url().ensure(),
twitter: yup.string().url().ensure(),
discord: yup.string().url().ensure(),
github: yup.string().url().ensure(),
category_id: yup.number().required("Please choose a category"),
capabilities: yup.array().of(yup.string().required()).default([]),
screenshots: yup.array().of(yup.string().required()).default([]),
members: yup.array().of(yup.object() as any).default([]),
recruit_roles: yup.array().default([]),
launch_status: yup.mixed().oneOf(['wip', 'launched']).default('wip'),
tournaments: yup.array().default([])
}).required();
export default function FormContainer(props: PropsWithChildren<Props>) {
const [params] = useSearchParams();
const methods = useForm<IListProjectForm>({
defaultValues: {
id: !!params.get('id') ? Number(params.get('id')) : undefined,
capabilities: [],
screenshots: [],
members: [],
recruit_roles: [],
launch_status: 'wip',
tournaments: [],
},
resolver: yupResolver(schema) as Resolver<IListProjectForm>,
});
const onSubmit: SubmitHandler<IListProjectForm> = data => console.log(data);
return (
<FormProvider {...methods} >
<form onSubmit={methods.handleSubmit(onSubmit)}>
{props.children}
</form>
</FormProvider>
)
}

View File

@@ -1,290 +1,188 @@
import { Controller, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"
import Button from "src/Components/Button/Button";
import { Project, User, useUpdateProfileAboutMutation } from "src/graphql";
import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { usePrompt } from "src/utils/hooks";
import { toast } from "react-toastify";
import { Controller, useFormContext } from "react-hook-form"
import Card from "src/Components/Card/Card";
import { FaDiscord, FaTwitter } from "react-icons/fa";
import { FaDiscord } from "react-icons/fa";
import { FiCamera, FiGithub, FiTwitter } from "react-icons/fi";
import CategoriesInput from "../CategoriesInput/CategoriesInput";
import CapabilitiesInput from "../CapabilitiesInput/CapabilitiesInput";
import { IListProjectForm } from "../FormContainer/FormContainer";
interface Props { }
export default function ProjectDetailsTab(props: Props) {
const { register, formState: { errors, }, control } = useFormContext<IListProjectForm>();
interface IProjectDetails {
name: 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[]>
}
interface Props {
data?: IProjectDetails,
onClose?: () => void;
}
// type IFormInputs = Props['data'];
const schema: yup.SchemaOf<IProjectDetails> = yup.object({
name: yup.string().trim().required().min(2),
website: yup.string().trim().url().required(),
tagline: yup.string().trim().required().min(10),
description: yup.string().trim().required().min(50, 'Write at least 10 words descriping your project'),
thumbnail_image: yup.string().url().required(),
cover_image: yup.string().url().required(),
twitter: yup.string().ensure(),
discord: yup.string().ensure(),
github: yup.string().ensure(),
category_id: yup.number().required(),
capabilities: yup.array().of(yup.string().required()),
screenshots: yup.array().of(yup.string().required()),
}).required();
export default function ProjectDetailsTab({ data, onClose }: Props) {
const { register, formState: { errors, isDirty, }, handleSubmit, reset, control } = useForm<IProjectDetails>({
defaultValues: {
...data,
capabilities: data?.capabilities ?? []
},
resolver: yupResolver(schema) as Resolver<IProjectDetails>,
// mode: 'onBlur',
});
const [mutate, mutationStatus] = useUpdateProfileAboutMutation();
// usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
const onSubmit: SubmitHandler<IProjectDetails> = data => {
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
mutate({
// variables: {
// data: {
// // name: data.name,
// // avatar: data.avatar,
// // jobTitle: data.jobTitle,
// // bio: data.bio,
// // email: data.email,
// // github: data.github,
// // linkedin: data.linkedin,
// // lightning_address: data.lightning_address,
// // location: data.location,
// // twitter: data.twitter,
// // website: data.website,
// }
// },
onCompleted: () => {
reset(data);
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
}
})
.catch(() => {
toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false });
mutationStatus.reset()
})
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="md:col-span-2 flex flex-col gap-24">
<Card className="" defaultPadding={false}>
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
{/* <Avatar src={data.avatar} width={120} /> */}
<div
className="rounded-full w-[120px] aspect-square border-2 border-gray-200 bg-white flex flex-col gap-8 items-center justify-center"
role={'button'}
>
<FiCamera className="text-gray-400 text-h2" />
<span className="text-gray-400 text-body6">Add image</span>
</div>
<div className="md:col-span-2 flex flex-col gap-24">
<Card className="" defaultPadding={false}>
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
{/* <Avatar src={data.avatar} width={120} /> */}
<div
className="rounded-full w-[120px] aspect-square border-2 border-gray-200 bg-white flex flex-col gap-8 items-center justify-center"
role={'button'}
>
<FiCamera className="text-gray-400 text-h2" />
<span className="text-gray-400 text-body6">Add image</span>
</div>
</div>
<div className="p-16 md:p-24 mt-64">
<p className="text-body5 font-medium">
Project name<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='e.g BOLT🔩FUN'
{...register("name")}
/>
</div>
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Project link<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://lightning.xyz'
{...register("website")}
/>
</div>
{errors.website && <p className="input-error">
{errors.website.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Tagline<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='Your products one liner goes here...'
{...register("tagline")}
/>
</div>
{errors.tagline && <p className="input-error">
{errors.tagline.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Description<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
placeholder='Provide a short description your product...'
{...register("description")}
/>
</div>
{errors.description && <p className="input-error">
{errors.description.message}
</p>}
</div>
</Card>
<Card className="">
<h2 className="text-body2 font-bolder">🔗 Links</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Make sure that people can find your project online. </p>
<div className="flex flex-col gap-8 mt-24">
<div>
<div className="input-wrapper mt-8 relative">
<FiTwitter className="text-blue-400 h-full flex-shrink-0 w-42 pl-12 py-12 self-center" />
<input
type='text'
className="input-text"
placeholder='https://twitter.com/'
{...register("twitter")}
/>
</div>
{errors.twitter && <p className="input-error">
{errors.twitter.message}
</p>}
</div>
<div>
<div className="input-wrapper mt-8 relative">
<FaDiscord className="text-violet-500 h-full flex-shrink-0 w-42 pl-12 py-12 self-center" />
<input
type='text'
className="input-text"
placeholder='https://discord.com/'
{...register("discord")}
/>
</div>
{errors.discord && <p className="input-error">
{errors.discord.message}
</p>}
</div>
<div>
<div className="input-wrapper mt-8 relative">
<FiGithub className="text-gray-700 h-full flex-shrink-0 w-42 pl-12 py-12 self-center" />
<input
type='text'
className="input-text"
placeholder='https://github.com/'
{...register("github")}
/>
</div>
{errors.github && <p className="input-error">
{errors.github.message}
</p>}
</div>
</div>
</Card>
<Card>
<h2 className="text-body2 font-bolder">🌶 Category</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Select one of the categories below.</p>
<div className="mt-24">
<Controller
control={control}
name="category_id"
render={({ field: { onChange, value } }) => (
<CategoriesInput
value={value}
onChange={onChange}
/>
)}
</div>
<div className="p-16 md:p-24 mt-64">
<p className="text-body5 font-medium">
Project name<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='e.g BOLT🔩FUN'
{...register("name")}
/>
{errors.category_id && <p className='input-error'>{errors.category_id?.message}</p>}
</div>
</Card>
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Project link<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<input
<Card>
<h2 className="text-body2 font-bolder">🦾 Capabilities</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Let other makers know what lightning capabilities your applicaiton has.</p>
<div className="mt-24">
<Controller
control={control}
name="capabilities"
render={({ field: { onChange, value } }) => (
<CapabilitiesInput
value={value}
onChange={onChange}
/>
)}
type='text'
className="input-text"
placeholder='https://lightning.xyz'
{...register("website")}
/>
{errors.capabilities && <p className='input-error'>{errors.capabilities?.message}</p>}
</div>
</Card>
</div>
<div className="self-start sticky-side-element">
{/* <SaveChangesCard
isLoading={mutationStatus.loading}
isDirty={isDirty}
onSubmit={handleSubmit(onSubmit)}
onCancel={() => reset()}
/> */}
</div>
{errors.website && <p className="input-error">
{errors.website.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Tagline<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='Your products one liner goes here...'
{...register("tagline")}
/>
</div>
{errors.tagline && <p className="input-error">
{errors.tagline.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Description<sup className="text-red-500">*</sup>
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
placeholder='Provide a short description your product...'
{...register("description")}
/>
</div>
{errors.description && <p className="input-error">
{errors.description.message}
</p>}
</div>
</Card>
<Card className="">
<h2 className="text-body2 font-bolder">🔗 Links</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Make sure that people can find your project online. </p>
<div className="flex flex-col gap-8 mt-24">
<div>
<div className="input-wrapper mt-8 relative">
<FiTwitter className="text-blue-400 h-full flex-shrink-0 w-42 pl-12 py-12 self-center" />
<input
type='text'
className="input-text"
placeholder='https://twitter.com/'
{...register("twitter")}
/>
</div>
{errors.twitter && <p className="input-error">
{errors.twitter.message}
</p>}
</div>
<div>
<div className="input-wrapper mt-8 relative">
<FaDiscord className="text-violet-500 h-full flex-shrink-0 w-42 pl-12 py-12 self-center" />
<input
type='text'
className="input-text"
placeholder='https://discord.com/'
{...register("discord")}
/>
</div>
{errors.discord && <p className="input-error">
{errors.discord.message}
</p>}
</div>
<div>
<div className="input-wrapper mt-8 relative">
<FiGithub className="text-gray-700 h-full flex-shrink-0 w-42 pl-12 py-12 self-center" />
<input
type='text'
className="input-text"
placeholder='https://github.com/'
{...register("github")}
/>
</div>
{errors.github && <p className="input-error">
{errors.github.message}
</p>}
</div>
</div>
</Card>
<Card>
<h2 className="text-body2 font-bolder">🌶 Category</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Select one of the categories below.</p>
<div className="mt-24">
<Controller
control={control}
name="category_id"
render={({ field: { onChange, value } }) => (
<CategoriesInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.category_id && <p className='input-error'>{errors.category_id?.message}</p>}
</div>
</Card>
<Card>
<h2 className="text-body2 font-bolder">🦾 Capabilities</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Let other makers know what lightning capabilities your applicaiton has.</p>
<div className="mt-24">
<Controller
control={control}
name="capabilities"
render={({ field: { onChange, value } }) => (
<CapabilitiesInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.capabilities && <p className='input-error'>{errors.capabilities?.message}</p>}
</div>
</Card>
</div>
)
}

View File

@@ -0,0 +1,105 @@
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 { tabs } from '../../ListProjectPage'
import { NotificationsService } from 'src/services'
interface Props {
currentTab: keyof typeof tabs
}
export default function SaveChangesCard(props: Props) {
const { handleSubmit, formState: { errors, isDirty, }, reset, getValues, watch } = useFormContext<IListProjectForm>();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const isUpdating = useMemo(() => !!getValues('id'), [getValues]);
const [img, name, tagline] = watch(['thumbnail_image', 'name', 'tagline'])
const clickCancel = () => {
if (window.confirm('You might lose some unsaved changes. Are you sure you want to continue?'))
// props.onCancel?.()
alert("Canceled")
}
const clickSubmit = handleSubmit<IListProjectForm>(data => {
NotificationsService.success("Submitted successfully")
console.log(data);
}, () => {
NotificationsService.error("Please fill all the required fields");
navigate(tabs['project-details'].path)
})
let ctaBtn = useMemo(() => {
if (isUpdating)
return <Button
color="primary"
fullWidth
onClick={clickSubmit}
disabled={!isDirty || isLoading}
>
Save Changes
</Button>
else if (props.currentTab === 'project-details')
return <Button
color="primary"
fullWidth
href={tabs.team.path}
>
Next step: {tabs.team.text}
</Button>
else if (props.currentTab === 'team')
return <Button
color="primary"
fullWidth
href={tabs.extras.path}
>
Next step: {tabs.extras.text}
</Button>
else
return <Button
color="primary"
fullWidth
onClick={clickSubmit}
disabled={!isDirty || isLoading}
>
List your product
</Button>
}, [clickSubmit, isDirty, isLoading, isUpdating, props.currentTab])
return (
<Card onlyMd className='flex flex-col gap-24'>
<div className='hidden md:flex gap-8'>
{img ?
<Avatar width={48} src={img} /> :
<div className="bg-gray-100 rounded-full w-48 h-48 shrink-0"></div>
}
<div className='overflow-hidden'>
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{name || "Product preview"}</p>
{<p className={`text-body6 text-gray-600`}>{tagline || "Provide some more details."}</p>}
</div>
</div>
{/* <p className="hidden md:block text-body5">{trimText(profileQuery.data.profile.bio, 120)}</p> */}
<div className="flex flex-col gap-16">
{ctaBtn}
<Button
color="gray"
onClick={clickCancel}
disabled={!isDirty || isLoading}
>
Cancel
</Button>
</div>
</Card>
)
}

View File

@@ -1,14 +1,13 @@
import { Menu, MenuButton, MenuItem } from '@szhsin/react-menu';
import React, { ComponentProps } from 'react'
import { ComponentProps } from 'react'
import { NestedValue } from 'react-hook-form'
import { FaChevronDown, FaRegTrashAlt, FaTimes } from 'react-icons/fa';
import IconButton from 'src/Components/IconButton/IconButton';
import { FaChevronDown, FaRegTrashAlt, } from 'react-icons/fa';
import UsersInput from 'src/Components/Inputs/UsersInput/UsersInput'
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
import { Team_Member_Role } from 'src/graphql';
import { ITeamForm } from '../TeamTab/TeamTab'
import { IListProjectForm } from '../FormContainer/FormContainer';
type Value = ITeamForm['members'] extends NestedValue<infer U> ? U : never;
type Value = IListProjectForm['members'] extends NestedValue<infer U> ? U : never;
type Props = {
value: Value,

View File

@@ -1,140 +1,63 @@
import { Controller, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"
import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { usePrompt } from "src/utils/hooks";
import { toast } from "react-toastify";
import { Controller, useFormContext } from "react-hook-form"
import Card from "src/Components/Card/Card";
import TeamMembersInput from "../TeamMembersInput/TeamMembersInput";
import { Team_Member_Role } from "src/graphql";
import RecruitRolesInput from "../RecruitRolesInput/RecruitRolesInput";
import { IListProjectForm } from "../FormContainer/FormContainer";
export interface ITeamForm {
members: NestedValue<{
id: number,
name: string,
jobTitle: string | null,
avatar: string,
role: Team_Member_Role,
}[]>
recruit_roles: NestedValue<string[]>
}
interface Props {
data?: ITeamForm,
}
// type IFormInputs = Props['data'];
export default function TeamTab(props: Props) {
const schema: yup.SchemaOf<ITeamForm> = yup.object({
members: yup.array().of(yup.object() as any).required().default([]),
recruit_roles: yup.array().required().default([])
}).required();
export default function TeamTab({ data }: Props) {
const { formState: { errors, isDirty, }, handleSubmit, reset, control } = useForm<ITeamForm>({
defaultValues: {
...data,
members: [],
recruit_roles: [],
},
resolver: yupResolver(schema) as Resolver<ITeamForm>,
// mode: 'onBlur',
});
const { formState: { errors, }, control } = useFormContext<IListProjectForm>();
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
const onSubmit: SubmitHandler<ITeamForm> = data => {
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
const mutate: any = null;
mutate({
// variables: {
// data: {
// // name: data.name,
// // avatar: data.avatar,
// // jobTitle: data.jobTitle,
// // bio: data.bio,
// // email: data.email,
// // github: data.github,
// // linkedin: data.linkedin,
// // lightning_address: data.lightning_address,
// // location: data.location,
// // twitter: data.twitter,
// // website: data.website,
// }
// },
onCompleted: () => {
reset(data);
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
}
})
.catch(() => {
toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false });
// mutationStatus.reset()
})
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="md:col-span-2 flex flex-col gap-24">
<div className="flex flex-col gap-24">
<Card>
<h2 className="text-body2 font-bolder"> Team</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Let us know who is on this products team.</p>
<div className="mt-24">
<Controller
control={control}
name="members"
render={({ field: { onChange, value } }) => (
<TeamMembersInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.members && <p className='input-error'>{errors.members?.message}</p>}
</div>
<div className="bg-gray-50 p-16 rounded-12 border border-gray-200 mt-24">
<p className="text-body5">
<span className="font-bold">💡 Onboard your team:</span> Make sure you onboard any other team members so they can help you manage this project.
</p>
</div>
</Card>
<Card >
<h2 className="text-body2 font-bolder"> Team</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Let us know who is on this products team.</p>
<div className="mt-24">
<Controller
control={control}
name="members"
render={({ field: { onChange, value } }) => (
<TeamMembersInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.members && <p className='input-error'>{errors.members?.message}</p>}
</div>
<div className="bg-gray-50 p-16 rounded-12 border border-gray-200 mt-24">
<p className="text-body5">
<span className="font-bold">💡 Onboard your team:</span> Make sure you onboard any other team members so they can help you manage this project.
</p>
</div>
</Card>
<Card>
<h2 className="text-body2 font-bolder">💪 Recruit</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Are you looking to recruit more makers to your project? Select the roles youre looking for below and let makers discover your project at Tournaments.</p>
<div className="mt-24">
<Controller
control={control}
name="recruit_roles"
render={({ field: { onChange, value } }) => (
<RecruitRolesInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.recruit_roles && <p className='input-error'>{errors.recruit_roles?.message}</p>}
</div>
</Card>
</div>
<div className="self-start sticky-side-element">
{/* <SaveChangesCard
isLoading={mutationStatus.loading}
isDirty={isDirty}
onSubmit={handleSubmit(onSubmit)}
onCancel={() => reset()}
/> */}
</div>
<Card>
<h2 className="text-body2 font-bolder">💪 Recruit</h2>
<p className="text-body4 font-light text-gray-600 mt-8">Are you looking to recruit more makers to your project? Select the roles youre looking for below and let makers discover your project at Tournaments.</p>
<div className="mt-24">
<Controller
control={control}
name="recruit_roles"
render={({ field: { onChange, value } }) => (
<RecruitRolesInput
value={value}
onChange={onChange}
/>
)}
/>
{errors.recruit_roles && <p className='input-error'>{errors.recruit_roles?.message}</p>}
</div>
</Card>
</div>
)
}

View File

@@ -1,4 +1,4 @@
import { Navigate, NavLink, Route, Routes, useParams } from "react-router-dom";
import { Navigate, NavLink, Route, Routes, useLocation, useParams } from "react-router-dom";
import Slider from "src/Components/Slider/Slider";
import { useAppSelector, useMediaQuery } from "src/utils/hooks";
import { Helmet } from 'react-helmet'
@@ -7,33 +7,47 @@ import Card from "src/Components/Card/Card";
import ProjectDetailsTab from "./Components/ProjectDetailsTab/ProjectDetailsTab";
import TeamTab from "./Components/TeamTab/TeamTab";
import ExtrasTab from "./Components/ExtrasTab/ExtrasTab";
import FormContainer from "./Components/FormContainer/FormContainer";
import { useMemo } from "react";
import SaveChangesCard from "./Components/SaveChangesCard/SaveChangesCard";
const links = [
{
export const tabs = {
'project-details': {
text: "🚀️ Project details",
path: 'project-details',
},
{
'team': {
text: "⚡️ Team",
path: 'team',
},
{
'extras': {
text: "💎 Extras",
path: 'extras',
}
]
} as const;
const links = [tabs['project-details'], tabs['team'], tabs['extras']];
type TabsKeys = keyof typeof tabs;
const getCurrentTab = (locattion: string) => {
for (const key in tabs) {
const tab = tabs[key as TabsKeys];
if (locattion.includes(tab.path))
return key as TabsKeys;
}
return null;
}
export default function ListProjectPage() {
const { id } = useParams()
const isUpdating = !!id;
const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
const location = useLocation();
const currentTab = useMemo(() => getCurrentTab(location.pathname), [location.pathname])
@@ -50,8 +64,8 @@ export default function ListProjectPage() {
<meta property="og:title" content='List a project' />
</Helmet>
<div className="page-container grid grid-cols-1 md:grid-cols-4 gap-24">
<aside>
{isMediumScreen ?
{isMediumScreen ?
<aside >
<Card className="sticky-side-element">
<p className="text-body2 font-bolder text-black mb-16">List a project</p>
<ul className=' flex flex-col gap-8'>
@@ -69,33 +83,44 @@ export default function ListProjectPage() {
</li>)}
</ul>
</Card>
:
<div className="border-b-2 border-gray-200">
<Slider>
{links.map((link, idx) =>
<NavLink
to={link.path}
key={idx}
className={`flex items-start cursor-pointer font-bold py-12
</aside>
:
<aside
className="border-b-2 border-gray-200 bg-white z-10 w-full sticky-top-element"
>
<Slider>
{links.map((link, idx) =>
<NavLink
to={link.path}
key={idx}
className={`flex items-start cursor-pointer font-bold py-12
active:scale-95 transition-transform`}
style={({ isActive }) => ({
boxShadow: isActive ? '0px 2px var(--primary)' : 'none'
})}
>
{link.text}
</NavLink>
)}
</Slider>
</div>
}
</aside>
style={({ isActive }) => ({
boxShadow: isActive ? '0px 2px var(--primary)' : 'none'
})}
>
{link.text}
</NavLink>
)}
</Slider>
</aside>
}
<main className="md:col-span-3">
<Routes>
<Route index element={<Navigate to={links[0].path} />} />
<Route path={links[0].path} element={<ProjectDetailsTab />} />
<Route path={links[1].path} element={<TeamTab />} />
<Route path={links[2].path} element={<ExtrasTab />} />
</Routes>
<FormContainer>
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="md:col-span-2">
<Routes>
<Route index element={<Navigate to={tabs['project-details'].path} />} />
<Route path={tabs['project-details'].path} element={<ProjectDetailsTab />} />
<Route path={tabs['team'].path} element={<TeamTab />} />
<Route path={tabs['extras'].path} element={<ExtrasTab />} />
</Routes>
</div>
<div className="self-start sticky-side-element">
{currentTab && <SaveChangesCard currentTab={currentTab} />}
</div>
</div>
</FormContainer>
</main>
</div>
</>

View File

@@ -25,7 +25,8 @@ export class NotificationsService {
toast.success(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
icon: "✅"
icon: "✅",
...options,
})
}
@@ -33,6 +34,15 @@ export class NotificationsService {
toast.info(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
...options,
})
}
static warn(msg: string, options?: AlertOptions) {
toast.warn(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
...options,
})
}
@@ -41,6 +51,7 @@ export class NotificationsService {
toast.error(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
...options,
})
}

View File

@@ -19,3 +19,8 @@ button[disabled] {
top: calc(var(--navHeight) + 16px);
max-height: calc(100vh - var(--navHeight) - 16px);
}
.sticky-top-element {
position: sticky;
top: calc(var(--navHeight));
}

View File

@@ -31,6 +31,10 @@ type RouteOptions =
id: string | number,
username?: string,
}
| {
type: "edit-project",
id?: number,
}
export function createRoute(options: RouteOptions) {
@@ -57,6 +61,9 @@ export function createRoute(options: RouteOptions) {
return `/profile/${options.id}`
+ (options.username ? `/${toSlug(options.username)}` : "");
if (options.type === 'edit-project')
return `/projects/list-project` + (options.id ? `?id=${options.id}` : '')
return ""
}