Created the new proposal form dialog

This commit is contained in:
2023-03-25 04:08:09 -05:00
parent a1c2c9c030
commit 0a71f91311
7 changed files with 173 additions and 123 deletions

View File

@@ -1,64 +1,25 @@
import { typo } from "@styles/typography";
import { cva } from "class-variance-authority";
import { useState } from "react";
import type { ZodString } from "zod";
import React from "react";
const input = cva([
typo({ tag: "p" }),
"w-full rounded border-[6px] py-2 px-3 md:py-3 md:px-4 leading-tight border-fg placeholder-fg/50 bg-fg/20",
"text-r-lg w-full rounded border-[6px] py-2 px-3 md:py-3 md:px-4 leading-tight border-fg placeholder-fg/50 bg-fg/20",
"focus:outline-none",
]);
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
placeholder?: string;
error?: string;
prefixIcon?: React.ReactNode;
suffixIcon?: React.ReactNode;
validator?: ZodString;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
value?: string;
}
export const TextInput: React.FC<Props> = ({
label,
placeholder,
prefixIcon,
suffixIcon,
validator,
onChange,
value: valueProp,
...rest
}) => {
const [value, setValue] = useState<string | undefined>(valueProp);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setValue(newValue);
if (onChange) {
onChange(event);
}
};
// let validationError: string | null = null;
// if (validator) {
// const result = validator.safeParse(value);
// if (result.success) {
// result.data;
// } else {
// validationError = result.error.message;
// }
// }
export const TextInput = React.forwardRef<HTMLInputElement, Props>(
({ label, placeholder, error, prefixIcon, suffixIcon, ...rest }, ref) => {
return (
<div className="w-full">
<fieldset className="w-full">
{label && (
<label
className={typo({ size: "base", className: "mb-1 block text-fg" })}
>
{label}
</label>
<label className="text-r-lg mb-1 block text-fg">{label}</label>
)}
<div
className={input({
@@ -69,16 +30,20 @@ export const TextInput: React.FC<Props> = ({
<input
type="text"
placeholder={placeholder}
value={value ?? ""}
onChange={handleChange}
className="w-full appearance-none border-none bg-transparent leading-tight text-fg placeholder-fg/50 focus:outline-none"
ref={ref}
{...rest}
/>
{suffixIcon && suffixIcon}
</div>
{/* {validationError && (
<span className="validation-error">{validationError}</span>
)} */}
</div>
{error && (
<p className="text-r-xl overflow-clip overflow-ellipsis whitespace-nowrap text-error">
{error}
</p>
)}
</fieldset>
);
};
}
);
TextInput.displayName = "TextInput";

View File

@@ -1,5 +1,4 @@
import type { Children } from "@utils/types/props";
import { typo } from "@styles/typography";
import Link from "next/link";
import {
FiMenu,
@@ -19,6 +18,14 @@ import { cx } from "class-variance-authority";
import { TextInput } from "@components/TextInput";
import { Divider } from "@components/Divider";
import { ImageInput } from "@components/ImageInput";
import { api } from "@utils/api";
import { getRootContainer } from "@constants/elements";
import toast from "react-hot-toast";
import { SubmitErrorHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { proposalSchema } from "@constants/schema";
import type { z } from "zod";
import type { SubmitHandler } from "react-hook-form";
export const MainLayout: React.FC<Children> = ({ children }) => {
return (
@@ -55,10 +62,7 @@ export const SidePanel: React.FC = () => {
<header className="flex h-screen flex-col justify-between gap-y-8 px-12 py-8">
<div>
<h1>
<Link
href="/"
className={typo({ tag: "h3", className: "text-primary" })}
>
<Link href="/" className="text-r-3xl text-primary">
<span className="text-fg">||</span> Parallel
</Link>
</h1>
@@ -67,11 +71,7 @@ export const SidePanel: React.FC = () => {
<Link
key={label}
href={route}
className={typo({
size: "2xl",
className:
"flex flex-row items-center gap-x-2 font-medium hover:font-bold hover:text-fg-400",
})}
className="text-r-2xl flex flex-row items-center gap-x-2 font-medium hover:font-bold hover:text-fg-400"
>
<Icon size={28} /> {label}
</Link>
@@ -85,9 +85,39 @@ export const SidePanel: React.FC = () => {
);
};
type ProposalForm = z.infer<typeof proposalSchema>;
const NewProposalButton: React.FC = () => {
const container =
typeof window !== "undefined" ? document.getElementById("root") : null;
// === Constants ============================================================
const container = getRootContainer();
// === Hooks ================================================================
const { handleSubmit, register, formState, reset } = useForm<ProposalForm>({
resolver: zodResolver(proposalSchema),
});
const ctx = api.useContext();
const { mutate, isLoading: isPosting } =
api.projects.createProposal.useMutation({
onSuccess: () => {
reset();
void ctx.projects.getAll.invalidate();
},
onError: (e) => {
toast.error(e.message);
},
});
// === Functions ============================================================
const onSubmit: SubmitHandler<ProposalForm> = (data): void => {
console.log("Submitting form data", data);
mutate(data);
};
// === Components ===========================================================
const FullDivider = () => (
<div className="my-4 -mx-10">
@@ -96,7 +126,11 @@ const NewProposalButton: React.FC = () => {
);
return (
<Dialog.Root>
<Dialog.Root
onOpenChange={(isOpen) => {
if (!isOpen) reset();
}}
>
<Dialog.Trigger asChild>
<Button variant={{ size: "small" }}>New Proposal</Button>
</Dialog.Trigger>
@@ -111,28 +145,38 @@ const NewProposalButton: React.FC = () => {
"focus:outline-none data-[state=open]:animate-contentShow"
)}
>
<Dialog.Title>
<h1 className={typo({ tag: "h5", className: "text-primary" })}>
Create new proposal
</h1>
<Dialog.Title asChild>
<h1 className="text-r-xl text-primary">Create new proposal</h1>
</Dialog.Title>
<Dialog.Description>
<Dialog.Description asChild>
<p className="text-r-lg">
Let others know a little about your project idea.
</p>
</Dialog.Description>
<FullDivider />
{/* eslint-disable-next-line @typescript-eslint/no-misused-promises */}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-8 flex flex-col gap-y-4">
<TextInput label="Title" />
<TextInput
label="Title"
placeholder={proposalSchema.shape.title.description}
error={formState.errors.title?.message}
{...register("title")}
/>
<TextInput
label="Description"
placeholder="Describe your project"
placeholder={proposalSchema.shape.description.description}
error={formState.errors.description?.message}
{...register("description")}
/>
<ImageInput label="Image" />
{/* <ImageInput label="Image" /> */}
</div>
<Dialog.Close asChild>
<div className="flex flex-row justify-end">
<Button variant={{ size: "small" }}>Create</Button>
<Button variant={{ size: "small" }} type="submit">
Create
</Button>
</div>
</Dialog.Close>
</form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
@@ -140,19 +184,12 @@ const NewProposalButton: React.FC = () => {
};
const MoreMenu: React.FC = () => {
const container =
typeof window !== "undefined" ? document.getElementById("root") : null;
const container = getRootContainer();
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
className={typo({
size: "2xl",
className:
"flex flex-row items-center gap-x-2 font-medium hover:font-bold hover:text-fg-400",
})}
>
<button className="text-r-2xl flex flex-row items-center gap-x-2 font-medium hover:font-bold hover:text-fg-400">
<FiMenu size={28} /> More
</button>
</DropdownMenu.Trigger>
@@ -163,8 +200,8 @@ const MoreMenu: React.FC = () => {
className="min-w-[250px] rounded-md bg-bg-700 p-3 data-[side=top]:animate-slideUpAndFade"
sideOffset={10}
>
<DropdownMenu.Item className="flex select-none items-center justify-between px-[5px] pl-[25px] text-lg outline-none">
<p className={typo({ tag: "p" })}>Settings</p>
<DropdownMenu.Item className="flex select-none items-center justify-between px-[5px] pl-[25px] outline-none">
<p className="text-r-lg">Settings</p>
<FiSettings />
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@@ -0,0 +1,2 @@
export const getRootContainer = () =>
typeof window !== "undefined" ? document.getElementById("root") : null;

22
src/constants/schema.ts Normal file
View File

@@ -0,0 +1,22 @@
import { z } from "zod";
export const proposalSchema = z.object({
title: z
.string({
description: "Give your project a title",
required_error: "Title is required",
invalid_type_error: "Title must be a string",
})
.trim()
.min(5, "Titles must be at least 5 characters long")
.max(64, "Titles must be 64 characters or less"),
description: z
.string({
description: "Describe your project",
required_error: "Description is required",
invalid_type_error: "Description must be a string",
})
.trim()
.min(5, "Descriptions must be at least 5 characters long")
.max(1028, "Descriptions must be 1028 characters or less"),
});

View File

@@ -6,6 +6,7 @@ import Head from "next/head";
import { RootLayout } from "@components/layouts";
import { api } from "@utils/api";
import "@styles/globals.css";
import { Toaster } from "react-hot-toast";
const ParallelApp: AppType<{ session: Session | null }> = ({
Component,
@@ -41,6 +42,7 @@ const ParallelApp: AppType<{ session: Session | null }> = ({
<SSRProvider>
<SessionProvider session={session}>
<RootLayout>
<Toaster position="top-center" />
<Component {...pageProps} />
</RootLayout>
</SessionProvider>

View File

@@ -1,6 +1,5 @@
import { DiscoverLayout } from "@components/layouts";
import { getServerAuthSession } from "@server/auth";
import { typo } from "@styles/typography";
import { api } from "@utils/api";
import type {
GetServerSideProps,
@@ -8,21 +7,30 @@ import type {
InferGetServerSidePropsType,
NextPage,
} from "next";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
const Discover: NextPage<
InferGetServerSidePropsType<typeof getServerSideProps>
> = () => {
const { data } = api.projects.getAll.useQuery();
const { data, isLoading } = api.projects.getAll.useQuery();
return (
<DiscoverLayout>
<h2 className={typo({ tag: "h2", className: "mb-8" })}>Favorites</h2>
{data?.map((project) => (
<div key={project.id}>
<h4 className={typo({ tag: "h4" })}>{project.title}</h4>
<p>{project.description}</p>
<h2 className="text-r-4xl mb-8">Favorites</h2>
{isLoading ? (
<p>Loading ...</p>
) : (
data?.map((project) => (
<div key={project.id} className="mb-4">
<h4 className="text-r-2xl">{project.title}</h4>
<p className="text-r-sm">{dayjs(project.createdAt).fromNow()}</p>
<p className="text-r-lg">{project.description}</p>
</div>
))}
))
)}
</DiscoverLayout>
);
};

View File

@@ -1,9 +1,23 @@
import { z } from "zod";
import { proposalSchema } from "@constants/schema";
import { createTRPCRouter, publicProcedure, protectedProcedure } from "../trpc";
export const projectsRouter = createTRPCRouter({
getAll: publicProcedure.query(({ ctx }) => {
return ctx.prisma.project.findMany({ take: 100 });
return ctx.prisma.project.findMany({
take: 100,
orderBy: [{ createdAt: "desc" }],
});
}),
createProposal: protectedProcedure
.input(proposalSchema)
.mutation(async ({ ctx, input }) => {
const authorId = ctx.session.user.id;
const proposal = await ctx.prisma.project.create({
data: { authorId, title: input.title, description: input.description },
});
return proposal;
}),
});