From 0a71f913119f9e79d50704c3d39da88134c73c86 Mon Sep 17 00:00:00 2001 From: Zeke Abshire Date: Sat, 25 Mar 2023 04:08:09 -0500 Subject: [PATCH] Created the new proposal form dialog --- src/components/TextInput.tsx | 105 ++++++++-------------- src/components/layouts/MainLayout.tsx | 121 +++++++++++++++++--------- src/constants/elements.ts | 2 + src/constants/schema.ts | 22 +++++ src/pages/_app.tsx | 2 + src/pages/discover/index.tsx | 26 ++++-- src/server/api/routers/projects.ts | 18 +++- 7 files changed, 173 insertions(+), 123 deletions(-) create mode 100644 src/constants/elements.ts create mode 100644 src/constants/schema.ts diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index e39f628..26600f6 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -1,84 +1,49 @@ -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 { label?: string; placeholder?: string; + error?: string; prefixIcon?: React.ReactNode; suffixIcon?: React.ReactNode; - validator?: ZodString; - onChange?: React.ChangeEventHandler; - value?: string; } -export const TextInput: React.FC = ({ - label, - placeholder, - prefixIcon, - suffixIcon, - validator, - onChange, - value: valueProp, - ...rest -}) => { - const [value, setValue] = useState(valueProp); - - const handleChange = (event: React.ChangeEvent) => { - 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; - // } - // } - - return ( -
- {label && ( -
+ {error && ( +

+ {error} +

+ )} + + ); + } +); + +TextInput.displayName = "TextInput"; diff --git a/src/components/layouts/MainLayout.tsx b/src/components/layouts/MainLayout.tsx index 7df6ed6..85e7074 100644 --- a/src/components/layouts/MainLayout.tsx +++ b/src/components/layouts/MainLayout.tsx @@ -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 }) => { return ( @@ -55,10 +62,7 @@ export const SidePanel: React.FC = () => {

- + || Parallel

@@ -67,11 +71,7 @@ export const SidePanel: React.FC = () => { {label} @@ -85,9 +85,39 @@ export const SidePanel: React.FC = () => { ); }; +type ProposalForm = z.infer; + const NewProposalButton: React.FC = () => { - const container = - typeof window !== "undefined" ? document.getElementById("root") : null; + // === Constants ============================================================ + + const container = getRootContainer(); + + // === Hooks ================================================================ + + const { handleSubmit, register, formState, reset } = useForm({ + 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 = (data): void => { + console.log("Submitting form data", data); + mutate(data); + }; + // === Components =========================================================== const FullDivider = () => (
@@ -96,7 +126,11 @@ const NewProposalButton: React.FC = () => { ); return ( - + { + if (!isOpen) reset(); + }} + > @@ -111,28 +145,38 @@ const NewProposalButton: React.FC = () => { "focus:outline-none data-[state=open]:animate-contentShow" )} > - -

- Create new proposal -

+ +

Create new proposal

- - Let others know a little about your project idea. + +

+ Let others know a little about your project idea. +

-
- - - -
- -
- + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} +
+
+ + + {/* */}
- +
+ +
+
@@ -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 ( - @@ -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} > - -

Settings

+ +

Settings

diff --git a/src/constants/elements.ts b/src/constants/elements.ts new file mode 100644 index 0000000..a936644 --- /dev/null +++ b/src/constants/elements.ts @@ -0,0 +1,2 @@ +export const getRootContainer = () => + typeof window !== "undefined" ? document.getElementById("root") : null; diff --git a/src/constants/schema.ts b/src/constants/schema.ts new file mode 100644 index 0000000..edc6185 --- /dev/null +++ b/src/constants/schema.ts @@ -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"), +}); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 1e1dc56..9665f45 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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 }> = ({ + diff --git a/src/pages/discover/index.tsx b/src/pages/discover/index.tsx index 6132502..183b6fd 100644 --- a/src/pages/discover/index.tsx +++ b/src/pages/discover/index.tsx @@ -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 > = () => { - const { data } = api.projects.getAll.useQuery(); + const { data, isLoading } = api.projects.getAll.useQuery(); return ( -

Favorites

- {data?.map((project) => ( -
-

{project.title}

-

{project.description}

-
- ))} +

Favorites

+ {isLoading ? ( +

Loading ...

+ ) : ( + data?.map((project) => ( +
+

{project.title}

+

{dayjs(project.createdAt).fromNow()}

+

{project.description}

+
+ )) + )}
); }; diff --git a/src/server/api/routers/projects.ts b/src/server/api/routers/projects.ts index a309375..0cf903c 100644 --- a/src/server/api/routers/projects.ts +++ b/src/server/api/routers/projects.ts @@ -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; + }), });