Skip to content

Commit

Permalink
Improve add target placeholder page (#29)
Browse files Browse the repository at this point in the history
* fixed button

* Changed add-target page design

* Redesigned page

* Insert waiting list item to db

* send emails

* Changed validation to zod

* fixed validation schemas

* add disabled state

* Fixed type

* Updated pnpm version in github action

* Small improvements

* Fixed comments
  • Loading branch information
SashaKryzh authored Sep 20, 2023
1 parent c9c83e7 commit 9796b27
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 28 deletions.
6 changes: 6 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ NEXT_PUBLIC_IMAGE_BUCKET_URL=/images
NEXT_PUBLIC_DD_APPLICATION_ID=app-id
NEXT_PUBLIC_DD_CLIENT_TOKEN=client-token
NEXT_PUBLIC_DD_ENV=dev

# Nodemailer
# 1. Google account should have "2-Factor Authentication" enabled
# 2. Create an "App Password" for the Google account <https://myaccount.google.com/u/4/apppasswords>
NODEMAILER_PSW=google_app_password
NODEMAILER_EMAIL=email_to_send_from
4 changes: 2 additions & 2 deletions .github/workflows/super-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

- uses: pnpm/action-setup@v2
with:
version: 7
version: 8

- name: install node
uses: actions/setup-node@v3
Expand All @@ -37,7 +37,7 @@ jobs:

- uses: pnpm/action-setup@v2
with:
version: 7
version: 8

- name: install node
uses: actions/setup-node@v3
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"lucide-react": "^0.279.0",
"next": "13.4.19",
"next-superjson-plugin": "^0.5.9",
"nodemailer": "^6.9.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^13.2.2",
Expand All @@ -41,10 +42,12 @@
"tailwindcss-animate": "^1.0.7",
"yet-another-react-lightbox": "^3.12.2",
"yup": "^1.2.0",
"zod": "^3.22.2"
"zod": "^3.22.2",
"zod-formik-adapter": "^1.2.0"
},
"devDependencies": {
"@types/node": "^20.6.2",
"@types/nodemailer": "^6.4.10",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.0",
Expand Down
30 changes: 30 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,9 @@ model EvidenceImage {
@@index([evidenceId])
}

model WaitingList {
id Int @id @default(autoincrement())
email String @unique
submitCount Int @default(1)
}
2 changes: 2 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ export default function Footer() {
<div className='flex gap-2'>
<Link
href='https://github.com/SashaKryzh'
target='_blank'
className='font-mono hover:underline'
>
@sashakryzh
</Link>
&
<Link
href='https://github.com/denitdao'
target='_blank'
className='font-mono hover:underline'
>
@denitdao
Expand Down
5 changes: 5 additions & 0 deletions src/components/Head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export default function Head({
<meta name='description' content={description} />
<link rel='icon' href='/favicon.ico' />

<meta
name='viewport'
content='width=device-width, initial-scale=1, maximum-scale=1'
/>

<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:type' content='website' />
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Header() {
<nav className='bg-white px-2 py-4'>
<div className='mx-auto flex max-w-screen-md flex-wrap items-center justify-between'>
<Link href='/'>UA Validator</Link>
<Button variant='ghost' asChild>
<Button variant='ghost' className='px-0' asChild>
<Link href='/add-target'>Додати</Link>
</Button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const buttonVariants = cva(
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
Expand Down
2 changes: 2 additions & 0 deletions src/components/ui/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type InputProps = React.ComponentPropsWithRef<'input'> &
suffixNode?: React.ReactNode;
};

// TODO: Use empty placeholder if placeholderLabel is provided for animation to work.

export const Input = forwardRef<HTMLInputElement, InputProps>(
({ variant, className, ...props }, ref) => {
const { prefixNode, suffixNode, ...rest } = props;
Expand Down
106 changes: 87 additions & 19 deletions src/pages/add-target.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
import { Head, Layout } from '@/components';
import InputField from '@/components/InputField';
import { Button } from '@/components/ui/Button';
import { GradientContainer } from '@/ui/GradientContainer';
import Spacer from '@/ui/Spacer';
import { trpc } from '@/utils/trpc';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import { MdNotificationsNone } from 'react-icons/md';
import z from 'zod';
import { toFormikValidationSchema } from 'zod-formik-adapter';
import type { NextPageWithLayout } from './_app';

const AddTarget: NextPageWithLayout = () => {
return (
<>
<main className='mx-auto max-w-screen-sm px-2'>
<Head title={'Додати людину'} />
<Spacer className='h-12' />
<div className='mx-auto max-w-screen-sm px-2 '>
<GradientContainer>
<div className=' px-4 py-5 text-sm font-light'>
<span className='font-normal'>
На жаль, ця частина ще не готова
</span>
, але Ви можете відправити нам інформацію на пошту{' '}
<a
className='font-mono text-blue-600'
href='mailto:[email protected]'
>
[email protected]
</a>{' '}
🙂
</div>
</GradientContainer>
<div className='h-7 md:h-12' />
<div className='font-light'>
<span className='font-normal'>На жаль, ця частина ще не готова</span>,
але Ви можете відправити нам інформацію на пошту{' '}
<a
className='font-mono text-blue-600'
href='mailto:[email protected]'
>
[email protected]
</a>{' '}
🙂
<br />
<br />
Або...
<br />
<br />
</div>
</>
<div className='h-2' />
<NotifyUser />
</main>
);
};

Expand All @@ -34,3 +43,62 @@ AddTarget.getLayout = (page) => {
};

export default AddTarget;

const validationSchema = z.object({
email: z
.string({ required_error: 'Уведіть email' })
.email('Невалідний email'),
});

type EmailForm = z.infer<typeof validationSchema>;

function NotifyUser() {
const router = useRouter();
const mutation = trpc.waitingList.add.useMutation();

function handleSubmit(values: EmailForm) {
mutation.mutate(values, {
onSuccess() {
alert('Дякуємо! Ми повідомимо Вас, коли цей функціонал буде готовий.');
router.push('/');
},
onError() {
alert('Щось пішло не так, спробуйте, будь ласка, ще раз.');
},
});
}

return (
<Formik
initialValues={{ email: '' }}
onSubmit={handleSubmit}
validationSchema={toFormikValidationSchema(validationSchema)}
>
{({}) => (
<Form>
<GradientContainer className='px-4 md:px-10'>
<div className='h-6' />
<div className='flex items-center justify-center'>
<MdNotificationsNone size={50} />
</div>
<div className='h-4' />
<div className='text-center font-light'>
Ми повідомимо Вас, коли цей функціонал буде додано
</div>
<div className='h-4' />
<InputField name='email' type='email' placeholderLabel='Email' />
<div className='h-6' />
<Button
type='submit'
className='w-full rounded-full py-6'
disabled={mutation.isLoading}
>
Повідомте мене
</Button>
<div className='h-8' />
</GradientContainer>
</Form>
)}
</Formik>
);
}
5 changes: 5 additions & 0 deletions src/server/schema/wait-list.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from 'zod';

export const addToWaitListSchema = z.object({ email: z.string().email() });

export type AddToWaitListSchema = z.infer<typeof addToWaitListSchema>;
37 changes: 37 additions & 0 deletions src/server/sendMail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import nodemailer from 'nodemailer';
import type Mail from 'nodemailer/lib/mailer';
import type SMTPTransport from 'nodemailer/lib/smtp-transport';

const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.NODEMAILER_EMAIL,
pass: process.env.NODEMAILER_PSW,
},
});

export interface Message extends Omit<Mail.Options, 'from' | 'bcc'> {}

export function sendMail(
message: Message,
): Promise<SMTPTransport.SentMessageInfo | Error> {
const bcc = process.env.NODEMAILER_EMAIL;

const mail: Mail.Options = {
...message,
from: process.env.NODEMAILER_EMAIL,
bcc: bcc,
};

return new Promise((resolve, reject) => {
transporter.sendMail(mail, (err, info) => {
if (err) {
console.log(err);
reject(err);
} else {
console.log(info);
resolve(info);
}
});
});
}
2 changes: 2 additions & 0 deletions src/server/trpc/router/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { jobRouter } from './job';
import { nationalityRouter } from './nationality';
import { targetRouter } from './target';
import { viewOnWarRouter } from './viewOnWar';
import { waitingListRouter } from './waitingList';

export const appRouter = router({
target: targetRouter,
viewOnWar: viewOnWarRouter,
job: jobRouter,
nationality: nationalityRouter,
waitingList: waitingListRouter,
});

// export type definition of API
Expand Down
29 changes: 29 additions & 0 deletions src/server/trpc/router/waitingList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { sendMail, type Message } from '@/server/sendMail';
import { publicProcedure, router } from '../trpc';
import { addToWaitListSchema } from '@/server/schema/wait-list.schema';

export const waitingListRouter = router({
add: publicProcedure
.input(addToWaitListSchema)
.mutation(async ({ ctx, input }) => {
await ctx.prisma.waitingList.upsert({
where: { email: input.email },
update: {
submitCount: {
increment: 1,
},
},
create: {
email: input.email,
},
});

const message: Message = {
to: input.email,
subject: 'Вітаємо! Вас додано до списку очікування | 🇺🇦 UA validator',
text: 'Ми повідомимо Ваc, коли буде можливо додавати людей до списку.\n\n🇺🇦 UA validator',
};

await sendMail(message);
}),
});
Loading

1 comment on commit 9796b27

@vercel
Copy link

@vercel vercel bot commented on 9796b27 Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.