style: linting and formatting

This commit is contained in:
Riccardo
2023-11-29 19:44:39 +01:00
parent 74ec709155
commit 77a9d50927
23 changed files with 1452 additions and 127 deletions

View File

@@ -1,3 +1,22 @@
{ {
"extends": "next/core-web-vitals" "env": {
"browser": true,
"es2021": true
},
"extends": [
"next/core-web-vitals",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
}
} }

4
.husky/commit-msg Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

7
.husky/pre-commit Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn audit
yarn format
yarn lint
yarn typecheck

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"jsxSingleQuote": true,
"tabWidth": 2,
"arrowParens": "avoid",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@@ -14,22 +14,22 @@ export async function GET(request: Request) {
const response = await hackernewsApi(); const response = await hackernewsApi();
return new NextResponse(JSON.stringify(response), { return new NextResponse(JSON.stringify(response), {
status: 200, status: 200
}); });
} }
async function hackernewsApi() { async function hackernewsApi() {
const topstories: number[] = await fetch(topNews).then((res) => res.json()); const topstories: number[] = await fetch(topNews).then(res => res.json());
console.log('topstories', topstories); console.log('topstories', topstories);
const newsPromises = topstories const newsPromises = topstories
.splice(0, Number(process.env.NEWS_LIMIT)) .splice(0, Number(process.env.NEWS_LIMIT))
.map(async (id) => { .map(async id => {
console.log('id', id); console.log('id', id);
const sourceNews: z.infer<typeof NewsSchema> = await fetch( const sourceNews: z.infer<typeof NewsSchema> = await fetch(
singleNews(id) singleNews(id)
).then((res) => res.json()); ).then(res => res.json());
console.log('sourceNews', sourceNews); console.log('sourceNews', sourceNews);
@@ -42,7 +42,7 @@ async function hackernewsApi() {
by: sourceNews.by, by: sourceNews.by,
time: sourceNews.time, time: sourceNews.time,
url: sourceNews.url, url: sourceNews.url,
score: sourceNews.score, score: sourceNews.score
}, },
update: { update: {
title: sourceNews.title, title: sourceNews.title,
@@ -51,18 +51,18 @@ async function hackernewsApi() {
by: sourceNews.by, by: sourceNews.by,
time: sourceNews.time, time: sourceNews.time,
url: sourceNews.url, url: sourceNews.url,
score: sourceNews.score, score: sourceNews.score
}, },
where: { where: {
id, id
}, },
select: { select: {
id: true, id: true
}, }
}); });
}); });
const newsIds = await Promise.all(newsPromises); const newsIds = await Promise.all(newsPromises);
return newsIds.map((news) => news.id); return newsIds.map(news => news.id);
} }

View File

@@ -1,4 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
import prisma from '../../../prisma/prisma'; import prisma from '../../../prisma/prisma';
import { ResponseSchema, SubscribeFormSchema } from '../../utils/types'; import { ResponseSchema, SubscribeFormSchema } from '../../utils/types';
@@ -7,7 +8,8 @@ export async function POST(request: Request) {
const body = await request.json(); const body = await request.json();
const validation = SubscribeFormSchema.safeParse(body); const validation = SubscribeFormSchema.safeParse(body);
if (!validation.success) { if (!validation.success) {
return new Response('Bad request', { status: 400 }); const message = fromZodError(validation.error);
return new Response(message.message, { status: 400 });
} }
const { email, targetingAllowed } = validation.data; const { email, targetingAllowed } = validation.data;
@@ -15,18 +17,18 @@ export async function POST(request: Request) {
await prisma.user.upsert({ await prisma.user.upsert({
create: { create: {
email, email,
targetingAllowed, targetingAllowed
}, },
update: { update: {
targetingAllowed, targetingAllowed
}, },
where: { where: {
email, email
}, }
}); });
const message: z.infer<typeof ResponseSchema> = { const message: z.infer<typeof ResponseSchema> = {
message: `${email} subscribed!`, message: `${email} subscribed!`
}; };
return new Response(JSON.stringify(message), { status: 200 }); return new Response(JSON.stringify(message), { status: 200 });
} }

View File

@@ -1,4 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
import prisma from '../../../prisma/prisma'; import prisma from '../../../prisma/prisma';
import { ResponseSchema, UnsubscribeFormSchema } from '../../utils/types'; import { ResponseSchema, UnsubscribeFormSchema } from '../../utils/types';
@@ -7,7 +8,8 @@ export async function POST(request: Request) {
const body = await request.json(); const body = await request.json();
const validation = UnsubscribeFormSchema.safeParse(body); const validation = UnsubscribeFormSchema.safeParse(body);
if (!validation.success) { if (!validation.success) {
return new Response('Bad request', { status: 400 }); const message = fromZodError(validation.error);
return new Response(message.message, { status: 400 });
} }
const { email } = validation.data; const { email } = validation.data;
@@ -15,15 +17,15 @@ export async function POST(request: Request) {
try { try {
await prisma.user.delete({ await prisma.user.delete({
where: { where: {
email, email
}, }
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
const message: z.infer<typeof ResponseSchema> = { const message: z.infer<typeof ResponseSchema> = {
message: `${email} unsubscribe!`, message: `${email} unsubscribe!`
}; };
return new Response(JSON.stringify(message), { status: 200 }); return new Response(JSON.stringify(message), { status: 200 });
} }

View File

@@ -2,8 +2,18 @@ html,
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, font-family:
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -apple-system,
BlinkMacSystemFont,
Segoe UI,
Roboto,
Oxygen,
Ubuntu,
Cantarell,
Fira Sans,
Droid Sans,
Helvetica Neue,
sans-serif;
background: #1e1e1e; background: #1e1e1e;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@@ -28,8 +38,11 @@ body {
border-radius: 10px; border-radius: 10px;
width: 85%; width: 85%;
padding: 50px; padding: 50px;
box-shadow: 0 54px 55px rgb(78 78 78 / 25%), 0 -12px 30px rgb(78 78 78 / 25%), box-shadow:
0 4px 6px rgb(78 78 78 / 25%), 0 12px 13px rgb(78 78 78 / 25%), 0 54px 55px rgb(78 78 78 / 25%),
0 -12px 30px rgb(78 78 78 / 25%),
0 4px 6px rgb(78 78 78 / 25%),
0 12px 13px rgb(78 78 78 / 25%),
0 -3px 5px rgb(78 78 78 / 25%); 0 -3px 5px rgb(78 78 78 / 25%);
} }

View File

@@ -1,22 +1,22 @@
import type { Metadata } from 'next' import type { Metadata } from 'next';
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google';
import './globals.css' import './globals.css';
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Create Next App', title: 'Create Next App',
description: 'Generated by create next app', description: 'Generated by create next app'
} };
export default function RootLayout({ export default function RootLayout({
children, children
}: { }: {
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang='en'>
<body className={inter.className}>{children}</body> <body className={inter.className}>{children}</body>
</html> </html>
) );
} }

View File

@@ -9,8 +9,8 @@ export default function Home() {
return ( return (
<VerticalLayout> <VerticalLayout>
<h1>Home</h1> <h1>Home</h1>
<Button label="Subscribe" onClick={() => router.push('/subscribe')} /> <Button label='Subscribe' onClick={() => router.push('/subscribe')} />
<Button label="Unsubscribe" onClick={() => router.push('/unsubscribe')} /> <Button label='Unsubscribe' onClick={() => router.push('/unsubscribe')} />
</VerticalLayout> </VerticalLayout>
); );
} }

View File

@@ -18,12 +18,12 @@ export default function Home() {
const response = await fetch('/api/subscribe', { const response = await fetch('/api/subscribe', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
email: data.get('email'), email: data.get('email'),
targetingAllowed: isChecked, targetingAllowed: isChecked
}), })
}); });
if (!response?.ok) { if (!response?.ok) {
@@ -33,6 +33,8 @@ export default function Home() {
const formResponse: z.infer<typeof ResponseSchema> = const formResponse: z.infer<typeof ResponseSchema> =
await response.json(); await response.json();
console.log(formResponse);
router.push('/success'); router.push('/success');
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -46,34 +48,34 @@ export default function Home() {
return ( return (
<VerticalLayout> <VerticalLayout>
<form className="container" onSubmit={handleSubmit}> <form className='container' onSubmit={handleSubmit}>
<h1>Subscribe to newsletter</h1> <h1>Subscribe to newsletter</h1>
<div className="email block"> <div className='email block'>
<label htmlFor="frm-email">Email</label> <label htmlFor='frm-email'>Email</label>
<input <input
placeholder="example@email.com" placeholder='example@email.com'
id="email" id='email'
type="email" type='email'
name="email" name='email'
required required
/> />
</div> </div>
<div className="checkbox block"> <div className='checkbox block'>
<label htmlFor="frm-checkbox">Allow advertising</label> <label htmlFor='frm-checkbox'>Allow advertising</label>
<input <input
id="targetingAllowed" id='targetingAllowed'
type="checkbox" type='checkbox'
name="targetingAllowed" name='targetingAllowed'
checked={isChecked} checked={isChecked}
onChange={handleCheckboxChange} onChange={handleCheckboxChange}
/> />
</div> </div>
<div className="button block"> <div className='button block'>
<button type="submit">Subscribe</button> <button type='submit'>Subscribe</button>
</div> </div>
</form> </form>
<Button label="Home" onClick={() => router.push('/')} /> <Button label='Home' onClick={() => router.push('/')} />
<Button label="Unsubscribe" onClick={() => router.push('/unsubscribe')} /> <Button label='Unsubscribe' onClick={() => router.push('/unsubscribe')} />
</VerticalLayout> </VerticalLayout>
); );
} }

View File

@@ -1,4 +1,5 @@
import { useRouter } from 'next/router'; 'use client';
import { useRouter } from 'next/navigation';
import { Button } from '../../components/Button'; import { Button } from '../../components/Button';
import { VerticalLayout } from '../../components/VerticalLayout'; import { VerticalLayout } from '../../components/VerticalLayout';
@@ -8,7 +9,7 @@ export default function Home() {
return ( return (
<VerticalLayout> <VerticalLayout>
<h1>Success!</h1> <h1>Success!</h1>
<Button label="Home" onClick={() => router.push('/')} /> <Button label='Home' onClick={() => router.push('/')} />
</VerticalLayout> </VerticalLayout>
); );
} }

View File

@@ -17,11 +17,11 @@ export default function Home() {
const response = await fetch('/api/unsubscribe', { const response = await fetch('/api/unsubscribe', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
email: data.get('email'), email: data.get('email')
}), })
}); });
if (!response?.ok) { if (!response?.ok) {
@@ -31,6 +31,8 @@ export default function Home() {
const formResponse: z.infer<typeof ResponseSchema> = const formResponse: z.infer<typeof ResponseSchema> =
await response.json(); await response.json();
console.log(formResponse);
router.push('/success'); router.push('/success');
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -40,24 +42,24 @@ export default function Home() {
return ( return (
<VerticalLayout> <VerticalLayout>
<form className="container" onSubmit={handleSubmit}> <form className='container' onSubmit={handleSubmit}>
<h1>Unsubscribe newsletter</h1> <h1>Unsubscribe newsletter</h1>
<div className="email block"> <div className='email block'>
<label htmlFor="frm-email">Email</label> <label htmlFor='frm-email'>Email</label>
<input <input
placeholder="example@email.com" placeholder='example@email.com'
id="email" id='email'
type="email" type='email'
name="email" name='email'
required required
/> />
</div> </div>
<div className="button block"> <div className='button block'>
<button type="submit">Unsubscribe</button> <button type='submit'>Unsubscribe</button>
</div> </div>
</form> </form>
<Button label="Home" onClick={() => router.push('/')} /> <Button label='Home' onClick={() => router.push('/')} />
<Button label="Subscribe" onClick={() => router.push('/subscribe')} /> <Button label='Subscribe' onClick={() => router.push('/subscribe')} />
</VerticalLayout> </VerticalLayout>
); );
} }

View File

@@ -1,16 +1,16 @@
import { z } from 'zod'; import { z } from 'zod';
export const ResponseSchema = z.object({ export const ResponseSchema = z.object({
message: z.string(), message: z.string()
}); });
export const SubscribeFormSchema = z.object({ export const SubscribeFormSchema = z.object({
email: z.string().email(), email: z.string().email(),
targetingAllowed: z.boolean(), targetingAllowed: z.boolean()
}); });
export const UnsubscribeFormSchema = z.object({ export const UnsubscribeFormSchema = z.object({
email: z.string().email(), email: z.string().email()
}); });
export const NewsSchema = z.object({ export const NewsSchema = z.object({
@@ -21,5 +21,5 @@ export const NewsSchema = z.object({
by: z.string(), by: z.string(),
time: z.number(), time: z.number(),
url: z.string().optional(), url: z.string().optional(),
score: z.number(), score: z.number()
}); });

1
commitlint.config.ts Normal file
View File

@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View File

@@ -1,7 +1,7 @@
interface ButtonProps { type ButtonProps = {
label: string; label: string;
onClick: () => void; onClick: () => void;
} };
export const Button = ({ label, onClick }: ButtonProps) => ( export const Button = ({ label, onClick }: ButtonProps) => (
<button onClick={onClick} key={1} className="overflow-hidden rounded-md"> <button onClick={onClick} key={1} className="overflow-hidden rounded-md">

View File

@@ -2,10 +2,10 @@ export const VerticalLayout = ({ children }: { children: React.ReactNode }) => {
return ( return (
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
gap: '16px', gap: "16px",
}} }}
> >
{children} {children}

View File

@@ -1,4 +1,4 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {} const nextConfig = {};
module.exports = nextConfig module.exports = nextConfig;

View File

@@ -1,13 +1,16 @@
{ {
"name": "next-newsletter", "name": "next-newsletter",
"version": "0.1.0", "version": "0.1.0",
"private": true, "description": "Template for NodeJS APIs with TypeScript, with configurations for linting and testing",
"author": "riccardo.s@hey.com",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "prisma generate && next build", "build": "prisma generate && next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "prettier --config .prettierrc 'app/' --write",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"prepare": "husky install",
"vercel:link": "vercel link", "vercel:link": "vercel link",
"vercel:env": "vercel env pull .env", "vercel:env": "vercel env pull .env",
"prisma:push": "npx prisma db push", "prisma:push": "npx prisma db push",
@@ -15,21 +18,39 @@
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.6.0", "@prisma/client": "^5.6.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"next": "14.0.3", "next": "14.0.3",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"zod": "^3.22.4" "zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@typescript-eslint/parser": "^6.12.0",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.0.3", "eslint-config-next": "14.0.3",
"eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3",
"lint-staged": "^15.1.0",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.6.0", "prisma": "^5.6.0",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
},
"lint-staged": {
"*.ts": [
"eslint --quiet --fix"
],
"*.{json,ts}": [
"prettier --write --ignore-unknown"
]
} }
} }

View File

@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
// PrismaClient is attached to the `global` object in development to prevent // PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit. // exhausting your database connection limit.
@@ -10,6 +10,6 @@ const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient(); export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma; export default prisma;

View File

@@ -1,20 +1,20 @@
import type { Config } from 'tailwindcss' import type { Config } from "tailwindcss";
const config: Config = { const config: Config = {
content: [ content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}', "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
'./src/components/**/*.{js,ts,jsx,tsx,mdx}', "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
'./src/app/**/*.{js,ts,jsx,tsx,mdx}', "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { theme: {
extend: { extend: {
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
'gradient-conic': "gradient-conic":
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
}, },
}, },
}, },
plugins: [], plugins: [],
} };
export default config export default config;

1299
yarn.lock

File diff suppressed because it is too large Load Diff