mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-16 21:04:22 +01:00
feat: added form state management
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">It’s 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">It’s 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 product’s 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 product’s 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 product’s 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 product’s 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 you’re 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 you’re 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user