Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"react/display-name": "off"
}
}
37 changes: 37 additions & 0 deletions codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
overwrite: true,
schema: "https://sanavaulth-api.herokuapp.com/graphql",
documents: "./**/*.graphql",
generates: {
"src/types.ts": {
plugins: [
"typescript",
"typescript-operations",
"fragment-matcher"
],
config: {
apolooClientVersion: 3,
useExplicitTyping: true,
},
},
"./graphql.schema.json": {
plugins: ["introspection"],
},
"src/": {
preset: "near-operation-file",
presetConfig: {
extension: ".generated.tsx",
baseTypesPath: "types.ts",
},
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
config: {
withHooks: true,
}
}
}
};

export default config;
13 changes: 11 additions & 2 deletions components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ interface DialogProps {
children: ReactNode;
description?: ReactNode;
title: ReactNode;
open: boolean;
setOpen: (open: boolean) => void;
isCaptchaOpen: boolean;
}

export function Dialog({ button, children, description, title }: DialogProps) {
export function Dialog({ button, children, description, title, open, setOpen, isCaptchaOpen }: DialogProps) {
const checkCanClose = () => {
if(!open) setOpen(true);
if(isCaptchaOpen) setOpen(true);
if(open && !isCaptchaOpen) setOpen(false);
}

return (
<RadDialog.Root>
<RadDialog.Root open={open} onOpenChange={checkCanClose}>
<RadDialog.Trigger asChild>
{button}
</RadDialog.Trigger>
Expand Down
12 changes: 9 additions & 3 deletions components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { forwardRef } from "react";
import { twMerge } from "tailwind-merge";

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
name: string;
label: string;
className?: string;
}

export function Input({ name, label, ...props }: InputProps) {
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { name, label, className } = props;
return (
<fieldset className="border-gray-500 relative rounded-md border px-3 py-2 shadow-sm focus-within:ring-1 focus-within:ring-gray-200">
<label
Expand All @@ -16,12 +19,15 @@ export function Input({ name, label, ...props }: InputProps) {
</label>
<input
{...props}
ref={ref}
name={name}
className={twMerge(
"block w-full border-0 outline-0 p-0 text-white bg-inherit placeholder-gray-500 focus:ring-0 sm:text-sm",
props.className
className
)}
/>
</fieldset>
);
}
});

export default Input;
12 changes: 9 additions & 3 deletions components/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { forwardRef } from "react";
import { twMerge } from "tailwind-merge";

interface TextAreaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
name: string;
label: string;
className?: string;
}

export function TextArea({ name, label, ...props }: TextAreaProps) {
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>((props, ref) => {
const { name, label, className } = props;
return (
<fieldset className="border-gray-500 relative rounded-md border px-2 shadow-sm focus-within:ring-1 focus-within:ring-gray-200">
<label
Expand All @@ -17,12 +20,15 @@ export function TextArea({ name, label, ...props }: TextAreaProps) {
</label>
<textarea
{...props}
ref={ref}
name={name}
className={twMerge(
"block w-full border-0 outline-0 p-0 text-white bg-inherit placeholder-gray-500 focus:ring-0 sm:text-sm",
props.className
className
)}
/>
</fieldset>
);
}
})

export default TextArea;
123 changes: 89 additions & 34 deletions features/support-form/components/SupportForm.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,99 @@
import React from "react";
import React, { useState } from "react";
import { Button } from "components/Button";
import { Dialog } from "components/Dialog";
import { Input } from "components/Input";
import { TextArea } from "components/TextArea";
import Input from "components/Input";
import TextArea from "components/TextArea";
import { useTranslation } from "next-i18next";
import { useForm } from "react-hook-form";
import { useMessageStore } from "../hooks/useMessageStore";
import { MessageInput } from "../mutations/CreateMessage.generated";
import HCaptcha from "@hcaptcha/react-hcaptcha";

export function SupportForm() {
const { t } = useTranslation('common')
const { t } = useTranslation("common");
const { createMessage } = useMessageStore();

const { register, handleSubmit, reset } = useForm<MessageInput>();

const [isModalOpen, setModalOpen] = useState(false);
const [isCaptchaOpen, setCaptchaOpen] = useState(false);
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState("");

const onMessageCreate = async (formData: MessageInput) => {
if (!token) return setError("Please complete the captcha");

const { data, errors } = await createMessage(formData, token);
if (errors) return setError("Something went wrong");
if (data?.createMessage) {
setCaptchaOpen(false);
setModalOpen(false);
reset();
}
};

const onError = (err: string) => {
setError(err);
};

const onExpire = () => {
setError("Kod captcha wygasł. Spróbuj ponownie.");
};

return (
<Dialog title={t("supportForm.title")} description={t("supportForm.description")} button={<button className="absolute bottom-4 right-4">{t("supportForm.help")}</button>}>
<form className="grid gap-5">
<Input
type="text"
name="firstName"
placeholder={t("supportForm.namePlaceholder")}
label={t("name")}
/>
<Input
type="text"
name="surName"
placeholder={t("supportForm.surnamePlaceholder")}
label={t("surname")}
/>
<Input
type="email"
name="email"
placeholder={t("supportForm.emailPlaceholder")}
label={t("email")}
/>
<TextArea
name="text"
label={t("supportForm.request")}
cols={30}
rows={10}
placeholder={t("supportForm.textPlaceholder")}
className="mt-2 rounded py-1 px-1"
/>
<Button type="submit">{t("submit")}</Button>
</form>
<Dialog
title={t("supportForm.title")}
description={t("supportForm.description")}
button={
<button className="absolute bottom-4 right-4">
{t("supportForm.help")}
</button>
}
open={isModalOpen}
setOpen={setModalOpen}
isCaptchaOpen={isCaptchaOpen}
>
<form
className="grid gap-5"
onSubmit={handleSubmit(onMessageCreate)}
>
<Input
type="text"
{...register("name")}
placeholder={t("supportForm.namePlaceholder")}
label={t("name")}
/>
<Input
type="text"
{...register("surname")}
placeholder={t("supportForm.surnamePlaceholder")}
label={t("surname")}
/>
<Input
type="email"
{...register("email")}
placeholder={t("supportForm.emailPlaceholder")}
label={t("email")}
/>
<TextArea
{...register("content")}
label={t("supportForm.request")}
cols={30}
rows={10}
placeholder={t("supportForm.textPlaceholder")}
className="mt-2 rounded py-1 px-1"
/>
<HCaptcha
sitekey={`${process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}`}
onOpen={() => setCaptchaOpen(true)}
onClose={() => setCaptchaOpen(false)}
onVerify={setToken}
onError={onError}
onExpire={onExpire}
tabIndex={100}
/>
<Button type="submit">{t("submit")}</Button>
</form>
</Dialog>
);
}
71 changes: 71 additions & 0 deletions features/support-form/fragments/Message/Message.generated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as Types from '../../../../src/types';

import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};

export type Message = {
__typename?: 'Message';
content?: Maybe<Scalars['String']>;
email?: Maybe<Scalars['String']>;
id: Scalars['ID'];
name?: Maybe<Scalars['String']>;
surname?: Maybe<Scalars['String']>;
};

export type MessageInput = {
content: Scalars['String'];
email: Scalars['String'];
name: Scalars['String'];
surname: Scalars['String'];
};

export type Mutation = {
__typename?: 'Mutation';
createMessage?: Maybe<Message>;
deleteMessage?: Maybe<Scalars['String']>;
};


export type MutationCreateMessageArgs = {
captcha: Scalars['String'];
message: MessageInput;
};


export type MutationDeleteMessageArgs = {
id?: InputMaybe<Scalars['ID']>;
};

export type Query = {
__typename?: 'Query';
message?: Maybe<Message>;
messages?: Maybe<Array<Maybe<Message>>>;
};


export type QueryMessageArgs = {
id?: InputMaybe<Scalars['ID']>;
};

export type MessageFragment = { __typename?: 'Message', content?: string | null, name?: string | null, surname?: string | null, email?: string | null };

export const MessageFragmentDoc = gql`
fragment Message on Message {
content
name
surname
email
}
`;
6 changes: 6 additions & 0 deletions features/support-form/fragments/Message/Message.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fragment Message on Message {
content
name
surname
email
}
16 changes: 16 additions & 0 deletions features/support-form/hooks/useMessageStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MessageInput, useCreateMessageMutation } from "../mutations/CreateMessage.generated";

export function useMessageStore() {
const [create, {loading: loadingCreate}] = useCreateMessageMutation();
return {
createMessage(message: MessageInput, captcha: string) {
return create({
variables: {
message,
captcha
}
});
},
loading: loadingCreate
}
}
Loading